Skip to content

Conversation

@itaybre
Copy link
Contributor

@itaybre itaybre commented Jan 5, 2026

📜 Description

Changes:

  • Added SentryWatchdogTerminationLogic.swift with @objc support
  • Added SentryWatchdogTerminationTracker.swift with @objc support
  • Deleted old Objective-C implementations (.m and .h files)
  • Updated imports in Integration and other files to use Swift versions via
    SentrySwift.h
  • Updated test data to reference Swift class properties

💡 Motivation and Context

These changes are needed to convert SentryWatchdogTerminationIntegration to Swift

💚 How did you test it?

📝 Checklist

You have to check all boxes before merging:

  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

No public changes, #skip-changelog

Closes #7127

…tionTracker to Swift

Migrate the Logic and Tracker classes from Objective-C to Swift while keeping
the Integration in Objective-C for now. The Swift classes are marked with @objc
and @_spi(Private) to maintain compatibility with the existing Objective-C Integration.

Changes:
- Add SentryWatchdogTerminationLogic.swift with @objc support
- Add SentryWatchdogTerminationTracker.swift with @objc support
- Remove old Objective-C implementations
- Update imports in Integration and other files to use Swift versions
- Update test data to reference Swift class properties
- Update bridging header to remove obsolete imports
@codecov
Copy link

codecov bot commented Jan 5, 2026

Codecov Report

❌ Patch coverage is 91.45299% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.739%. Comparing base (8440c54) to head (ff7fa11).
⚠️ Report is 3 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...gTerminations/SentryWatchdogTerminationLogic.swift 82.142% 10 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@              Coverage Diff              @@
##              main     #7118       +/-   ##
=============================================
+ Coverage   84.710%   84.739%   +0.028%     
=============================================
  Files          459       459               
  Lines        27490     27515       +25     
  Branches     12117     12111        -6     
=============================================
+ Hits         23287     23316       +29     
+ Misses        4162      4157        -5     
- Partials        41        42        +1     
Files with missing lines Coverage Δ
Sources/Sentry/SentryClient.m 97.435% <100.000%> (+0.009%) ⬆️
Sources/Sentry/SentryCrashIntegration.m 100.000% <ø> (ø)
...rces/Sentry/SentryCrashIntegrationSessionHandler.m 100.000% <ø> (ø)
...try/SentryWatchdogTerminationTrackingIntegration.m 89.655% <ø> (ø)
...erminations/SentryWatchdogTerminationTracker.swift 100.000% <100.000%> (ø)
...gTerminations/SentryWatchdogTerminationLogic.swift 82.142% <82.142%> (ø)

... and 10 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8440c54...ff7fa11. Read the comment docs.

@itaybre itaybre added the ready-to-merge Use this label to trigger all PR workflows label Jan 5, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Jan 6, 2026

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1220.11 ms 1265.87 ms 45.76 ms
Size 24.14 KiB 1.03 MiB 1.01 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
718c372 1220.09 ms 1235.15 ms 15.06 ms
7f2f69c 1237.61 ms 1266.96 ms 29.35 ms
80f98b4 1229.95 ms 1252.68 ms 22.73 ms
6c0b61e 1194.21 ms 1218.74 ms 24.53 ms
fa27d5b 1195.50 ms 1218.19 ms 22.69 ms
9080e6c 1221.17 ms 1247.87 ms 26.71 ms
af7a86c 1220.82 ms 1243.36 ms 22.54 ms
5cbd333 1220.78 ms 1234.15 ms 13.36 ms
22af3fc 1220.16 ms 1250.25 ms 30.09 ms
b13e93a 1236.24 ms 1247.33 ms 11.08 ms

App size

Revision Plain With Sentry Diff
718c372 23.75 KiB 920.65 KiB 896.90 KiB
7f2f69c 23.75 KiB 913.38 KiB 889.63 KiB
80f98b4 24.14 KiB 1.02 MiB 1017.65 KiB
6c0b61e 23.75 KiB 1.02 MiB 1019.10 KiB
fa27d5b 24.14 KiB 1.01 MiB 1015.38 KiB
9080e6c 23.75 KiB 926.80 KiB 903.05 KiB
af7a86c 23.74 KiB 926.65 KiB 902.90 KiB
5cbd333 23.74 KiB 969.66 KiB 945.92 KiB
22af3fc 23.75 KiB 1.00 MiB 1002.40 KiB
b13e93a 23.75 KiB 855.37 KiB 831.62 KiB

@_implementationOnly import _SentryPrivate
import Foundation

#if (os(iOS) || os(tvOS) || os(visionOS)) && !SENTRY_NO_UIKIT
Copy link

Choose a reason for hiding this comment

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

Bug: The Swift compilation guard for watchdog termination tracking excludes Mac Catalyst, causing a runtime crash when Objective-C code tries to instantiate the missing Swift classes on that platform.
Severity: CRITICAL | Confidence: High

🔍 Detailed Analysis

The Swift classes SentryWatchdogTerminationLogic and SentryWatchdogTerminationTracker are conditionally compiled using a #if directive that excludes the Mac Catalyst target. However, existing Objective-C code, which is compiled for Mac Catalyst, attempts to instantiate these Swift classes. Because the classes are not included in the Mac Catalyst binary, this will cause a runtime crash when watchdog termination tracking is enabled on that platform. This is a regression introduced by the migration from Objective-C to Swift.

💡 Suggested Fix

Update the compilation guard in SentryWatchdogTerminationLogic.swift and SentryWatchdogTerminationTracker.swift to include the Mac Catalyst target. Change #if (os(iOS) || os(tvOS) || os(visionOS)) && !SENTRY_NO_UIKIT to #if (os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst)) && !SENTRY_NO_UIKIT.

🤖 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/Swift/Integrations/WatchdogTerminations/SentryWatchdogTerminationLogic.swift#L4

Potential issue: The Swift classes `SentryWatchdogTerminationLogic` and
`SentryWatchdogTerminationTracker` are conditionally compiled using a `#if` directive
that excludes the Mac Catalyst target. However, existing Objective-C code, which is
compiled for Mac Catalyst, attempts to instantiate these Swift classes. Because the
classes are not included in the Mac Catalyst binary, this will cause a runtime crash
when watchdog termination tracking is enabled on that platform. This is a regression
introduced by the migration from Objective-C to Swift.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 8304193

Copy link
Member

Choose a reason for hiding this comment

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

This needs to be verified!

let previousVendorId = previousAppState.vendorId,
currentVendorId != previousVendorId {
return false
}
Copy link

Choose a reason for hiding this comment

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

Vendor ID nil handling differs from original behavior

Medium Severity

The vendor ID check in the Swift conversion has different semantics than the original Objective-C code. In Objective-C, [nil isEqualToString:anything] and [anything isEqualToString:nil] both return NO, causing the original code to return NO (not a watchdog termination) whenever either vendorId was nil. The Swift if let pattern only returns false when BOTH vendor IDs are non-nil AND different. This means the Swift version will now continue checking and potentially report watchdog terminations in cases where one or both vendor IDs are nil, while the original would have rejected those cases.

Fix in Cursor Fix in Web

Copy link
Member

Choose a reason for hiding this comment

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

This is valid feedback! The new if-condition doesn't check fo the cases where one of the two values is nil, but the other is set

Copy link
Member

@philprime philprime left a comment

Choose a reason for hiding this comment

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

Good progress, left some comments to discuss

Comment on lines +14 to +16
@objc public static let SentryWatchdogTerminationExceptionType: String = "WatchdogTermination"
@objc public static let SentryWatchdogTerminationExceptionValue: String = "The OS watchdog terminated your app, possibly because it overused RAM."
@objc public static let SentryWatchdogTerminationMechanismType: String = "watchdog_termination"
Copy link
Member

Choose a reason for hiding this comment

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

l: These are already scoped to SentryWatchdogTerminationTracker so this leads to SentryWatchdogTerminationTracker.SentryWatchdogTerminationExceptionType which seems to be unnecessary

Comment on lines +39 to +72
@objc public func start() {
appStateManager.start()

dispatchQueue.dispatchAsync {
guard self.watchdogTerminationLogic.isWatchdogTermination() else {
return
}

let event = Event(level: .fatal)

self.addBreadcrumbs(to: event)
self.addContext(to: event)
event.user = self.scopePersistentStore.readPreviousUserFromDisk()
event.dist = self.scopePersistentStore.readPreviousDistFromDisk()
event.environment = self.scopePersistentStore.readPreviousEnvironmentFromDisk()
event.tags = self.scopePersistentStore.readPreviousTagsFromDisk()
event.extra = self.scopePersistentStore.readPreviousExtrasFromDisk()
event.fingerprint = self.scopePersistentStore.readPreviousFingerprintFromDisk()
// Termination events always have fatal level, so we are not reading from disk

let exception = Exception(
value: SentryWatchdogTerminationTracker.SentryWatchdogTerminationExceptionValue,
type: SentryWatchdogTerminationTracker.SentryWatchdogTerminationExceptionType)
let mechanism = Mechanism(type: SentryWatchdogTerminationTracker.SentryWatchdogTerminationMechanismType)
mechanism.handled = false
exception.mechanism = mechanism
event.exceptions = [exception]

// We don't need to update the releaseName of the event to the previous app state as we
// assume it's not a watchdog termination when the releaseName changed between app
// starts.
SentrySDKInternal.captureFatalEvent(event)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

m: We could reduce nesting and references to self:

Suggested change
@objc public func start() {
appStateManager.start()
dispatchQueue.dispatchAsync {
guard self.watchdogTerminationLogic.isWatchdogTermination() else {
return
}
let event = Event(level: .fatal)
self.addBreadcrumbs(to: event)
self.addContext(to: event)
event.user = self.scopePersistentStore.readPreviousUserFromDisk()
event.dist = self.scopePersistentStore.readPreviousDistFromDisk()
event.environment = self.scopePersistentStore.readPreviousEnvironmentFromDisk()
event.tags = self.scopePersistentStore.readPreviousTagsFromDisk()
event.extra = self.scopePersistentStore.readPreviousExtrasFromDisk()
event.fingerprint = self.scopePersistentStore.readPreviousFingerprintFromDisk()
// Termination events always have fatal level, so we are not reading from disk
let exception = Exception(
value: SentryWatchdogTerminationTracker.SentryWatchdogTerminationExceptionValue,
type: SentryWatchdogTerminationTracker.SentryWatchdogTerminationExceptionType)
let mechanism = Mechanism(type: SentryWatchdogTerminationTracker.SentryWatchdogTerminationMechanismType)
mechanism.handled = false
exception.mechanism = mechanism
event.exceptions = [exception]
// We don't need to update the releaseName of the event to the previous app state as we
// assume it's not a watchdog termination when the releaseName changed between app
// starts.
SentrySDKInternal.captureFatalEvent(event)
}
}
@objc public func start() {
appStateManager.start()
dispatchQueue.dispatchAsync {
self.captureStartEvent()
}
}
private func captureStartEvent() {
guard self.watchdogTerminationLogic.isWatchdogTermination() else {
return
}
let event = Event(level: .fatal)
addBreadcrumbs(to: event)
addContext(to: event)
event.user = scopePersistentStore.readPreviousUserFromDisk()
event.dist = scopePersistentStore.readPreviousDistFromDisk()
event.environment = scopePersistentStore.readPreviousEnvironmentFromDisk()
event.tags = scopePersistentStore.readPreviousTagsFromDisk()
event.extra = scopePersistentStore.readPreviousExtrasFromDisk()
event.fingerprint = scopePersistentStore.readPreviousFingerprintFromDisk()
// Termination events always have fatal level, so we are not reading from disk
let exception = Exception(
value: SentryWatchdogTerminationTracker.SentryWatchdogTerminationExceptionValue,
type: SentryWatchdogTerminationTracker.SentryWatchdogTerminationExceptionType)
let mechanism = Mechanism(type: SentryWatchdogTerminationTracker.SentryWatchdogTerminationMechanismType)
mechanism.handled = false
exception.mechanism = mechanism
event.exceptions = [exception]
// We don't need to update the releaseName of the event to the previous app state as we
// assume it's not a watchdog termination when the releaseName changed between app
// starts.
SentrySDKInternal.captureFatalEvent(event)
}

@_implementationOnly import _SentryPrivate
import Foundation

#if (os(iOS) || os(tvOS) || os(visionOS)) && !SENTRY_NO_UIKIT
Copy link
Member

Choose a reason for hiding this comment

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

This needs to be verified!

let previousVendorId = previousAppState.vendorId,
currentVendorId != previousVendorId {
return false
}
Copy link
Member

Choose a reason for hiding this comment

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

This is valid feedback! The new if-condition doesn't check fo the cases where one of the two values is nil, but the other is set


- (BOOL)isWatchdogTermination:(SentryEvent *)event isFatalEvent:(BOOL)isFatalEvent
{
#if SENTRY_HAS_UIKIT
Copy link
Member

Choose a reason for hiding this comment

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

h: TBH I do not like that we pull the availability of UIKit in here. Why does the caller of the watchdog termination tracker need to know if it's available with or without UIKit?

XCTAssertTrue(createMetricKitEventWith(mechanismType: "mx_hang_diagnostic").isMetricKitEvent())
}

#if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst)
Copy link
Member

Choose a reason for hiding this comment

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

m: Do not exclude the entire test, instead we should use a XCTSkip inside the test, so ideally the test suite is always the same, but we can see skips per platform

}

#if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst)
func testCaptureOOMEvent_RemovesMutableInfoFromDeviceContext() throws {
Copy link
Member

Choose a reason for hiding this comment

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

m: Do not conditionally exclude the test from compilation, instead us the condition inside the test with an XCTSkip so we can see skipped tests per platform

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-merge Use this label to trigger all PR workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Converts SentryWatchdogTerminationLogic and SentryWatchdogTerminationTracker to Swift

3 participants