-
-
Notifications
You must be signed in to change notification settings - Fork 375
Converts SentryWatchdogTerminationLogic and SentryWatchdogTerminationTracker to Swift
#7118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…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 Report❌ Patch coverage is
Additional details and impacted files@@ 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
... and 10 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
|
Performance metrics 🚀
|
| 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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
philprime
left a comment
There was a problem hiding this 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
| @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" |
There was a problem hiding this comment.
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
| @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) | ||
| } | ||
| } |
There was a problem hiding this comment.
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:
| @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 |
There was a problem hiding this comment.
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 | ||
| } |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
📜 Description
Changes:
SentrySwift.h
💡 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:
sendDefaultPIIis enabled.No public changes, #skip-changelog
Closes #7127