Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2b5250f
feat: install KSCrash with production-safe monitors in SentryKSCrashI…
NinjaLikesCheez Jul 16, 2026
8e2c53e
fix: remove KSCrashInstallations import from test target to break dep…
NinjaLikesCheez Jul 16, 2026
1d24690
fix: treat KSCrash alreadyInstalled error as success
NinjaLikesCheez Jul 16, 2026
84b4353
chore: abstract KSCrash to protocols so testutils doesn't need to imp…
NinjaLikesCheez Jul 16, 2026
9b22e6d
ref: rework the API to nest related types
NinjaLikesCheez Jul 20, 2026
59a2762
test: update to use swift testing because the future is now
NinjaLikesCheez Jul 20, 2026
f41cd5b
changelog: add changelog for feature
NinjaLikesCheez Jul 20, 2026
24c7263
chore: correctly disable arm64e for mac catalyst builds
NinjaLikesCheez Jul 20, 2026
4ca43bd
build: bump macOS deployment target to 10.15 for test targets only
NinjaLikesCheez Jul 21, 2026
f739144
test(kscrash): revert Swift Testing conversion back to XCTest
NinjaLikesCheez Jul 21, 2026
fec5a4d
test(kscrash): rename tests to follow project naming convention
NinjaLikesCheez Jul 21, 2026
130cfe3
fix(kscrash): expose productionSafeMonitors as UInt to fix test build
NinjaLikesCheez Jul 21, 2026
220f37c
chore: remove unused membership exception
NinjaLikesCheez Jul 21, 2026
731abd1
chore: mirror SentryCrash install path configuration
NinjaLikesCheez Jul 21, 2026
fbc56d5
chore: wire into SentrySDK's crash status reporting
NinjaLikesCheez Jul 21, 2026
3640f20
fix: use CFBundleName instead of CFBundleIdentifier
NinjaLikesCheez Jul 23, 2026
940d2e7
feat: separate 'fatalDetected' into 'crashHandlerDetectedCrash'
NinjaLikesCheez Jul 23, 2026
440b482
chore: sanitize file path in same way as SentryCrash (also fix name i…
NinjaLikesCheez Jul 23, 2026
a394bc2
test: reset state after tests
NinjaLikesCheez Jul 23, 2026
57c91fa
lint: appease the linting god for the rules hath changed under me
NinjaLikesCheez Jul 24, 2026
207f6c1
lint: disable the rule (fixed in a follow up PR) as the semantics of …
NinjaLikesCheez Jul 24, 2026
d975839
chore: extract install path to helper function to centralize path bui…
NinjaLikesCheez Jul 29, 2026
0c228c3
ref: extract installer to local variable
NinjaLikesCheez Jul 29, 2026
a23914d
test: don't clear test state on setup & tear down
NinjaLikesCheez Jul 31, 2026
2290319
chore: track installed status at the crash subsystem level
NinjaLikesCheez Jul 31, 2026
f37a279
feat: Add SentryKSCrash+Query to expose crash handler state (swift) t…
NinjaLikesCheez Jul 31, 2026
eb0fd17
test: update tests to use new SentryKSCrash & SDKInternal API
NinjaLikesCheez Jul 30, 2026
0c27969
test: add test to cover setting fatalDetected
NinjaLikesCheez Jul 31, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG_V10.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

- Install KSCrash crash handler in `Sentry+KSCrash` with production-safe monitors matching SentryCrash's existing monitor set (#8469)
- Add `options.dataCollection` to configure data collection behaviour (#8448)
- Allows dictionary initialization for `options.dataCollection` (#8371)
- Renamed `options.dataCollection.queryParams` to `options.dataCollection.urlQueryParams` (#8414)
Expand Down
1 change: 0 additions & 1 deletion SentryTestUtils/Sources/ClearTestState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ class TestCleanup: NSObject {
SentrySDK.close()
SentrySDKInternal.setCurrentHub(nil)
SentrySDKInternal.lastRunStatusCalled = false
SentrySDKInternal.crashReporterInstalled = false
SentrySDKInternal.fatalDetected = false
SentrySDKInternal.startInvocations = 0
SentrySDKInternal.setDetectedStartUpCrash(false)
Expand Down
10 changes: 9 additions & 1 deletion Sources/Configuration/SentryKSCrash.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,15 @@ MACH_O_TYPE = mh_dylib
// see https://github.com/getsentry/sentry-cocoa/pull/8426 for more context
ARCHS[sdk=appletvos*] = $(ARCHS_STANDARD)
ARCHS[sdk=watchos*] = $(ARCHS_STANDARD)
ARCHS[sdk=maccatalyst*] = $(ARCHS_STANDARD)

// Mac Catalyst is not affected by iOSPackagesShouldBuildARM64e, so KSCrash SPM packages
// never produce arm64e slices for maccatalyst. Mac Catalyst also doesn't have
// it's own SDK name, we so we need to do some working around to use the one
// build setting that shows Mac Catalyst is being build for: IS_MACCATALYST.
// Key off it via nested macro substitution instead.
SENTRY_KSCRASH_ARM64E_IF_NOT_CATALYST_YES =
SENTRY_KSCRASH_ARM64E_IF_NOT_CATALYST_NO = arm64e
ARCHS[sdk=macosx*] = arm64 x86_64 $(SENTRY_KSCRASH_ARM64E_IF_NOT_CATALYST_$(IS_MACCATALYST))

// Both this target and the base Sentry target share PRODUCT_MODULE_NAME=Sentry. Xcode's
// explicit module scanner can't tell them apart and emits a spurious missing-dependency
Expand Down
32 changes: 22 additions & 10 deletions Sources/Sentry/SentrySDKInternal.m
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
#import "SentrySwift.h"
#import "SentryTransactionContext.h"

#if ENABLE_KSCRASH
// Forward declared from SentryKSCrash+Query.swift
@interface SentryKSCrashQuery <NSObject>
@property (nonatomic, readonly) BOOL installed;
@property (nonatomic, readonly) BOOL crashedLastLaunch;
@end
#endif // ENABLE_KSCRASH

#if TARGET_OS_OSX
# import "SentryCrashExceptionApplication.h"
#endif // TARGET_OS_MAC
Expand All @@ -38,7 +46,6 @@ @implementation SentrySDKInternal
static SentryHubInternal *_Nullable currentHub;
static NSObject *currentHubLock;
static BOOL lastRunStatusCalled;
static BOOL crashReporterInstalled;
static BOOL fatalDetected;
static SentryAppStartMeasurement *_Nullable sentrySDKappStartMeasurement;
static NSObject *sentrySDKappStartMeasurementLock;
Expand Down Expand Up @@ -143,12 +150,11 @@ + (void)setLastRunStatusCalled:(BOOL)value

+ (BOOL)crashReporterInstalled
{
return crashReporterInstalled;
}

+ (void)setCrashReporterInstalled:(BOOL)value
{
crashReporterInstalled = value;
#if ENABLE_KSCRASH
return SentryDependencyContainer.sharedInstance.kscrashQuery.installed;
#else
return SentryDependencyContainer.sharedInstance.crashWrapper.installed;
#endif
}

+ (BOOL)fatalDetected
Expand Down Expand Up @@ -276,7 +282,7 @@ + (void)startWithOptions:(SentryOptions *)options
// The .didCrash case is handled by SentryClient.prepareEvent when
// a fatal event arrives. Here we report .didNotCrash if no
// integration set fatalDetected during init.
if (crashReporterInstalled && !fatalDetected) {
if ([SentrySDKInternal crashReporterInstalled] && !fatalDetected) {
lastRunStatusCalled = YES;
if (nil != options.onLastRunStatusDetermined) {
options.onLastRunStatusDetermined(SentryLastRunStatusDidNotCrash, nil);
Expand Down Expand Up @@ -502,12 +508,19 @@ + (BOOL)crashedLastRun

+ (NSInteger)lastRunStatus
{
if (!crashReporterInstalled) {
if (![SentrySDKInternal crashReporterInstalled]) {
return SentryLastRunStatusUnknown;
}

#if ENABLE_KSCRASH
if (SentryDependencyContainer.sharedInstance.kscrashQuery.crashedLastLaunch) {
return SentryLastRunStatusDidCrash;
}
#else
Comment on lines 510 to +519

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: When ENABLE_KSCRASH is active, crashedLastRun incorrectly reads from the disabled SentryCrash reporter, always returning false even after a crash.
Severity: MEDIUM

Suggested Fix

Use an #if ENABLE_KSCRASH preprocessor directive for the crashedLastRun property. When the flag is active, the property should get its value from SentryDependencyContainer.sharedInstance.kscrashQuery.crashedLastLaunch. Otherwise, it should use the existing SentryDependencyContainer.sharedInstance.crashReporter.crashedLastLaunch.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: Sources/Sentry/SentrySDKInternal.m#L510-L519

Potential issue: When the `ENABLE_KSCRASH` preprocessor flag is active, the
`crashedLastRun` property incorrectly continues to read from the `SentryCrash` reporter
via `crashReporter.crashedLastLaunch`. However, with `ENABLE_KSCRASH` enabled, the
`SentryCrashIntegration` is not installed, meaning the `crashReporter` is never set up.
Consequently, `crashedLastRun` will always return `false`, even if the app actually
crashed. This is inconsistent with other properties like `lastRunStatus`, which
correctly use a conditional compilation block to read from the appropriate KSCrash
source.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

i guess this is because +(BOOL) crashedLastRun is just above this, but it's compiled out in SDK_V10 mode (which ENABLE_KSCRASH includes).

if (SentryDependencyContainer.sharedInstance.crashWrapper.crashedLastLaunch) {
return SentryLastRunStatusDidCrash;
}
#endif
return SentryLastRunStatusDidNotCrash;
}

Expand Down Expand Up @@ -604,7 +617,6 @@ + (void)close

[SentrySDKInternal setCurrentHub:nil];

crashReporterInstalled = NO;
fatalDetected = NO;
lastRunStatusCalled = NO;

Expand Down
2 changes: 2 additions & 0 deletions Sources/Sentry/include/SentryCrash.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ SENTRY_NO_INIT
/** If true, the application crashed on the previous launch. */
@property (nonatomic, readonly, assign) BOOL crashedLastLaunch;

@property (nonatomic, readonly) BOOL installed;

/** The total number of unsent reports. Note: This is an expensive operation. */
@property (nonatomic, readonly, assign) int reportCount;

Expand Down
3 changes: 3 additions & 0 deletions Sources/SentryCrash/Recording/SentryCrash.m
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,8 @@ - (BOOL)install
object:nil];
#endif // SENTRY_HAS_NSEXTENSION

_installed = true;

return true;
}

Expand All @@ -300,6 +302,7 @@ - (void)uninstall
self.monitoringFromUninstalledToRestore = YES;
self.onCrash = NULL;
sentrycrash_uninstall();
_installed = false;

#if SENTRY_HAS_UIKIT
id<SentryNSNotificationCenterWrapper> notificationCenter
Expand Down
2 changes: 1 addition & 1 deletion Sources/Swift/Core/Integrations/Integrations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ private struct AnyIntegration {
#endif

#if ENABLE_KSCRASH
integrations.append(.init(SentryKSCrashIntegration.self))
integrations.append(.init(SentryKSCrash.Integration.self))
#else
integrations.append(.init(SentryCrashIntegration.self))
#endif
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#if ENABLE_KSCRASH
// swiftlint:disable:next no_implementation_only_import
@_implementationOnly import KSCrashInstallations

extension SentryKSCrash {
protocol Installing {
/// Install the crash handler.
/// - Parameters:
/// - installPath: The base directory for crash report storage.
/// - monitors: Monitor types to enable.
/// - enableSwapCxaThrow: Whether to swap `__cxa_throw` for better C++ stacks.
/// - Throws: Any error from `KSCrash.installWithConfiguration(_:error:)`.
func install(installPath: String, monitors: UInt, enableSwapCxaThrow: Bool) throws

/// Whether the previous run crashed.
var crashedLastLaunch: Bool { get }

var installed: Bool { get }
}

/// Configures and installs a crash handler
struct Installer: SentryKSCrash.Installing {
func install(installPath: String, monitors: UInt, enableSwapCxaThrow: Bool) throws {
let config = KSCrashConfiguration()
config.installPath = installPath
config.monitors = MonitorType(rawValue: monitors)
config.enableSwapCxaThrow = enableSwapCxaThrow
do {
try KSCrash.shared.install(with: config)
} catch let error as NSError
where error.domain == "KSCrashErrorDomain" && error.code == 1 /* KSCrashInstallErrorAlreadyInstalled */ {
// KSCrash holds a process-lifetime C flag, so install() fails on every
// subsequent call within the same process (common during tests and SDK re-init).
// The crash handler is already running — treat this as success.
SentrySDKLog.debug("KSCrash already installed; continuing.")
}
}

var crashedLastLaunch: Bool { KSCrash.shared.crashedLastLaunch }

var installed: Bool {
// From KSCrash docs: 'If the crash reporter is not installed, this will be `nil`'
KSCrash.shared.reportStore != nil
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#if ENABLE_KSCRASH
// swiftlint:disable:next no_implementation_only_import
@_implementationOnly import KSCrashInstallations
internal import _SentryPrivate
import Foundation

// MARK: - Integration
extension SentryKSCrash {
typealias DependencyProvider = SentryKSCrash.InstallerProvider

/// Crash detectors matching SentryCrash's production monitor set:
/// Mach exceptions, signals, C++ exceptions, and NSExceptions.
/// KSCrash unconditionally adds its Required monitors (System, AppState,
/// UserInfo, Resource) on top of whatever is passed here.
static let productionSafeMonitors: UInt = MonitorType([
.machException,
.signal,
.cppException,
.nsException
]).rawValue

final class Integration<Dependencies: DependencyProvider>: NSObject, SwiftIntegration {
private weak var options: Options?

// MARK: - Initialization

init?(with options: Options, dependencies: Dependencies) {
guard options.enableCrashHandler else {
SentrySDKLog.debug("Not going to enable \(Self.name) because enableCrashHandler is disabled.")
return nil
}

self.options = options
super.init()

let installer = dependencies.getKSCrashInstaller()

// To match KSCrash & SentryCrash, we need to add 'KSCrash/<bundlename>' to the cacheDirectoryPath
let installPath = Self.installPath(for: options.cacheDirectoryPath, bundleInfo: Bundle.main.infoDictionary)

do {
try installer.install(
installPath: installPath.path,
monitors: productionSafeMonitors,
enableSwapCxaThrow: options.experimental.enableUnhandledCPPExceptionsV2
)
Comment thread
cursor[bot] marked this conversation as resolved.
} catch {
SentrySDKLog.error("KSCrash install failed: \(error)")
return nil
}

if installer.crashedLastLaunch {
SentrySDKInternal.fatalDetected = true
}
Comment thread
cursor[bot] marked this conversation as resolved.
}

// MARK: - SwiftIntegration
static var name: String {
"SentryKSCrashIntegration"
}

func uninstall() {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

h: I guess we must set SentrySDKInternal.crashReporterInstalled = false here. BTW, the SentryCrashIntegration also doesn't call this on uninstall. Maybe worth double checking and fixing in another PR.


// MARK: - Helpers
/// Get the install path for the crash reporter
/// This method exists to centralize path building to save rebuilding it manually in tests
static func installPath(for cacheDirectoryPath: String, bundleInfo: [String: Any]?) -> URL {
let bundleName = bundleInfo?["CFBundleName"] as? String ?? "Unknown"
return URL(fileURLWithPath: cacheDirectoryPath)
.appendingPathComponent("KSCrash")
.appendingPathComponent(bundleName.replacingOccurrences(of: "/", with: "-"))
.absoluteURL
}
}
}
#endif
46 changes: 46 additions & 0 deletions Sources/Swift/Integrations/KSCrash/SentryKSCrash+Query.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#if ENABLE_KSCRASH
// swiftlint:disable:next no_implementation_only_import
@_implementationOnly import KSCrashInstallations
internal import _SentryPrivate

extension SentryKSCrash {
protocol QueryProvider {
associatedtype Querying: SentryKSCrash.Querying

var kscrashQuery: Querying { get }
}
}

extension SentryKSCrash {
/// Allows querying the state of the crash reporter
@objc(SentryKSCrashQuerying)
public protocol Querying: NSObjectProtocol {
/// Whether KSCrash has been successfully installed this session.
@objc var installed: Bool { get }
/// Whether KSCrash crashed on the previous launch.
@objc var crashedLastLaunch: Bool { get }
}
}

extension SentryKSCrash {
/// An ObjC-accessible class that surfaces live KSCrash crash state to ObjC callers
/// without adding `@objc` to `SentryKSCrash.Installer`.
@_spi(Private)
@objc(SentryKSCrashQuery)
final public class Query: NSObject, Querying {
private let installer: any Installing

init(installer: some Installing) {
self.installer = installer
}

// swiftlint:disable missing_docs
@objc
public var installed: Bool { installer.installed }

@objc
public var crashedLastLaunch: Bool { installer.crashedLastLaunch }
//swiftlint:enable missing_docs
}
}
#endif
12 changes: 12 additions & 0 deletions Sources/Swift/Integrations/KSCrash/SentryKSCrash.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#if ENABLE_KSCRASH
/// All types related to KSCrash should live under this type
@_spi(Private) public enum SentryKSCrash {
/// Provides a `KSCrashInstalling` instance for dependency injection.
protocol InstallerProvider {
/// The installer used to set up KSCrash crash reporting.
associatedtype Installing: SentryKSCrash.Installing

func getKSCrashInstaller() -> Installing
}
}
#endif

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ final class SentryCrashIntegration<Dependencies: CrashIntegrationProvider>: NSOb
// SentrySDK.lastRunStatus returns a definitive answer and the integration
// installer can determine the .didNotCrash case. We can't use
// isIntegrationInstalled because it's set after init returns.
SentrySDKInternal.crashReporterInstalled = true
if SentryDependencyContainer.sharedInstance().crashWrapper.crashedLastLaunch {
SentrySDKInternal.fatalDetected = true
}
Expand Down
1 change: 1 addition & 0 deletions Sources/Swift/SentryCrash/SentryCrashReporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import Foundation

@_spi(Private) @objc public protocol SentryCrashReporter: NSObjectProtocol {
@objc var installed: Bool { get }
@objc var crashedLastLaunch: Bool { get }
@objc var durationFromCrashStateInitToLastCrash: TimeInterval { get }
@objc var activeDurationSinceLastCrash: TimeInterval { get }
Expand Down
6 changes: 5 additions & 1 deletion Sources/Swift/SentryCrash/SentryCrashSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ private final class CrashReportFilterBridge: NSObject, SentryCrashReportFilter {
@objc public func install() {
sentryCrash.install()
}


@objc public var installed: Bool {
sentryCrash.installed
}

@objc public func uninstall() {
sentryCrash.uninstall()
}
Expand Down
Loading
Loading