From f6cfe6227cef8e01d8e0fd0ae7205e97ab270564 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 23 Mar 2026 13:53:58 +0100 Subject: [PATCH 1/4] feat(feedback): add runtime shake gesture toggle API Add SentrySDK.feedback.enableShakeGesture() and disableShakeGesture() for dynamic control at runtime. --- .../UserFeedback/SentryFeedbackAPI.swift | 16 +++++++ .../SentryUserFeedbackIntegrationDriver.swift | 10 +++++ .../Feedback/SentryShakeDetectorTests.swift | 18 ++++++++ sdk_api.json | 44 +++++++++++++++++++ 4 files changed, 88 insertions(+) diff --git a/Sources/Swift/Integrations/UserFeedback/SentryFeedbackAPI.swift b/Sources/Swift/Integrations/UserFeedback/SentryFeedbackAPI.swift index feeb14e4f04..5e76607a726 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryFeedbackAPI.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryFeedbackAPI.swift @@ -20,6 +20,22 @@ @objc public func hideWidget() { getIntegration()?.driver.hideWidget() } + + /// Enable shake gesture to show the feedback form. + /// Call this to dynamically enable shake-to-report at runtime. + /// - warning: This is an experimental feature and may still have bugs. + @available(iOSApplicationExtension, unavailable) + @objc public func enableShakeGesture() { + getIntegration()?.driver.enableShakeGesture() + } + + /// Disable shake gesture for the feedback form. + /// Call this to dynamically disable shake-to-report at runtime. + /// - warning: This is an experimental feature and may still have bugs. + @available(iOSApplicationExtension, unavailable) + @objc public func disableShakeGesture() { + getIntegration()?.driver.disableShakeGesture() + } private func getIntegration() -> UserFeedbackIntegration? { SentrySDKInternal.currentHub().getInstalledIntegration(UserFeedbackIntegration.self) as? UserFeedbackIntegration diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift index 7a4953ffc40..0b4fa48a64c 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift @@ -75,6 +75,16 @@ final class SentryUserFeedbackIntegrationDriver: NSObject { widget?.rootVC.setWidget(visible: false, animated: configuration.animations) } + func enableShakeGesture() { + SentryShakeDetector.enable() + NotificationCenter.default.addObserver(self, selector: #selector(handleShakeGesture), name: .SentryShakeDetected, object: nil) + } + + func disableShakeGesture() { + SentryShakeDetector.disable() + NotificationCenter.default.removeObserver(self, name: .SentryShakeDetected, object: nil) + } + @objc func showForm(sender: UIButton) { presenter?.present(SentryUserFeedbackFormController(config: configuration, delegate: self, screenshot: nil), animated: configuration.animations) { self.configuration.onFormOpen?() diff --git a/Tests/SentryTests/Integrations/Feedback/SentryShakeDetectorTests.swift b/Tests/SentryTests/Integrations/Feedback/SentryShakeDetectorTests.swift index 0213e4ea12c..ab20caab8d5 100644 --- a/Tests/SentryTests/Integrations/Feedback/SentryShakeDetectorTests.swift +++ b/Tests/SentryTests/Integrations/Feedback/SentryShakeDetectorTests.swift @@ -82,6 +82,24 @@ final class SentryShakeDetectorTests: XCTestCase { NotificationCenter.default.removeObserver(observer) } + func testReEnable_afterDisable_shouldPostNotification() { + SentryShakeDetector.enable() + SentryShakeDetector.disable() + SentryShakeDetector.enable() + + // Wait for cooldown from any previous test's shake + let cooldownExpectation = expectation(description: "cooldown") + cooldownExpectation.isInverted = true + wait(for: [cooldownExpectation], timeout: 1.1) + + let expectation = expectation(forNotification: .SentryShakeDetected, object: nil) + + let window = UIWindow() + window.motionEnded(.motionShake, with: nil) + + wait(for: [expectation], timeout: 1.0) + } + func testOriginalImplementation_shouldStillBeCalled() { SentryShakeDetector.enable() diff --git a/sdk_api.json b/sdk_api.json index 7d7b65d11f3..474558d1572 100644 --- a/sdk_api.json +++ b/sdk_api.json @@ -37230,6 +37230,50 @@ "printedName": "init()", "usr": "c:@M@Sentry@objc(cs)SentryFeedbackAPI(im)init" }, + { + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + } + ], + "declAttributes": [ + "Available", + "Final", + "ObjC" + ], + "declKind": "Func", + "funcSelfKind": "NonMutating", + "kind": "Function", + "mangledName": "$s6Sentry0A11FeedbackAPIC19disableShakeGestureyyF", + "moduleName": "Sentry", + "name": "disableShakeGesture", + "printedName": "disableShakeGesture()", + "usr": "c:@M@Sentry@objc(cs)SentryFeedbackAPI(im)disableShakeGesture" + }, + { + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + } + ], + "declAttributes": [ + "Available", + "Final", + "ObjC" + ], + "declKind": "Func", + "funcSelfKind": "NonMutating", + "kind": "Function", + "mangledName": "$s6Sentry0A11FeedbackAPIC18enableShakeGestureyyF", + "moduleName": "Sentry", + "name": "enableShakeGesture", + "printedName": "enableShakeGesture()", + "usr": "c:@M@Sentry@objc(cs)SentryFeedbackAPI(im)enableShakeGesture" + }, { "children": [ { From 3e4a038b76657521fa71ddd89fe46d23e2ef5b98 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 25 Mar 2026 09:04:24 +0100 Subject: [PATCH 2/4] feat(feedback): add changelog for runtime shake toggle --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25e9e4ab4db..bbe774d9158 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add `SentrySDK.feedback.enableShakeGesture()` and `SentrySDK.feedback.disableShakeGesture()` for runtime control of shake-to-report ([#7737](https://github.com/getsentry/sentry-cocoa/pull/7737)) + ## 9.8.0 ### Features From 9abe889f9943a2f0f70910ef95723e9a06478f44 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 25 Mar 2026 09:16:15 +0100 Subject: [PATCH 3/4] fix(feedback): prevent duplicate observers and improve test speed Remove existing shake observer before adding in enableShakeGesture() to prevent duplicate notifications on repeated calls or when useShakeGesture was set at init. Add test-only cooldown reset to eliminate 1.1s sleep in re-enable test. --- .../Integrations/UserFeedback/SentryShakeDetector.swift | 7 +++++++ .../UserFeedback/SentryUserFeedbackIntegrationDriver.swift | 2 ++ .../Integrations/Feedback/SentryShakeDetectorTests.swift | 6 +----- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift b/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift index a58746df8c4..d11b55e4e33 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift @@ -94,6 +94,13 @@ public final class SentryShakeDetector: NSObject { enabled = false SentrySDKLog.debug("Shake detector: disabled") } + + #if SENTRY_TEST + /// Resets cooldown state for testing. Not available in production builds. + static func resetCooldownForTesting() { + lastShakeTimestamp = 0 + } + #endif #else /// No-op on non-iOS platforms. @objc public static func enable() {} diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift index 0b4fa48a64c..36db4579724 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift @@ -76,6 +76,8 @@ final class SentryUserFeedbackIntegrationDriver: NSObject { } func enableShakeGesture() { + // Remove any existing observer first to prevent duplicate notifications + NotificationCenter.default.removeObserver(self, name: .SentryShakeDetected, object: nil) SentryShakeDetector.enable() NotificationCenter.default.addObserver(self, selector: #selector(handleShakeGesture), name: .SentryShakeDetected, object: nil) } diff --git a/Tests/SentryTests/Integrations/Feedback/SentryShakeDetectorTests.swift b/Tests/SentryTests/Integrations/Feedback/SentryShakeDetectorTests.swift index ab20caab8d5..9349dfde5c9 100644 --- a/Tests/SentryTests/Integrations/Feedback/SentryShakeDetectorTests.swift +++ b/Tests/SentryTests/Integrations/Feedback/SentryShakeDetectorTests.swift @@ -9,6 +9,7 @@ final class SentryShakeDetectorTests: XCTestCase { override func tearDown() { super.tearDown() SentryShakeDetector.disable() + SentryShakeDetector.resetCooldownForTesting() } func testEnable_whenShakeOccurs_shouldPostNotification() { @@ -87,11 +88,6 @@ final class SentryShakeDetectorTests: XCTestCase { SentryShakeDetector.disable() SentryShakeDetector.enable() - // Wait for cooldown from any previous test's shake - let cooldownExpectation = expectation(description: "cooldown") - cooldownExpectation.isInverted = true - wait(for: [cooldownExpectation], timeout: 1.1) - let expectation = expectation(forNotification: .SentryShakeDetected, object: nil) let window = UIWindow() From f1890ca3fab564c924daffb6b2f560765418fbb4 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 25 Mar 2026 09:24:02 +0100 Subject: [PATCH 4/4] fix(feedback): include SENTRY_TEST_CI for test-only API --- .../Swift/Integrations/UserFeedback/SentryShakeDetector.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift b/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift index d11b55e4e33..2a178571415 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift @@ -95,7 +95,7 @@ public final class SentryShakeDetector: NSObject { SentrySDKLog.debug("Shake detector: disabled") } - #if SENTRY_TEST + #if SENTRY_TEST || SENTRY_TEST_CI /// Resets cooldown state for testing. Not available in production builds. static func resetCooldownForTesting() { lastShakeTimestamp = 0