From 971f5a58405fee7f698a363e93ca47c5d715fe1f Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 3 Mar 2026 17:43:09 +0100 Subject: [PATCH 01/17] feat(feedback): implement shake gesture detection for user feedback form The useShakeGesture configuration property existed but was not implemented. This adds SentryShakeDetector which swizzles UIWindow.motionEnded:withEvent: to detect shake gestures and wires it into SentryUserFeedbackIntegrationDriver. Co-Authored-By: Claude Opus 4.6 --- Sentry.xcodeproj/project.pbxproj | 8 ++ Sources/Sentry/Public/Sentry.h | 1 + Sources/Sentry/Public/SentryShakeDetector.h | 45 +++++++++++ Sources/Sentry/SentryShakeDetector.m | 78 +++++++++++++++++++ .../SentryUserFeedbackIntegrationDriver.swift | 17 ++++ 5 files changed, 149 insertions(+) create mode 100644 Sources/Sentry/Public/SentryShakeDetector.h create mode 100644 Sources/Sentry/SentryShakeDetector.m diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index cb547207e23..de9a3ac4822 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -430,6 +430,8 @@ D88817DA26D72AB800BF2251 /* SentryTraceContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D88817D926D72AB800BF2251 /* SentryTraceContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; D8A3649C2C91AA3300AC569B /* SentryReplayApi.m in Sources */ = {isa = PBXBuildFile; fileRef = D8A3649B2C91AA3300AC569B /* SentryReplayApi.m */; }; D8A3649D2C91AA3300AC569B /* SentryReplayApi.h in Headers */ = {isa = PBXBuildFile; fileRef = D8A3649A2C91AA3300AC569B /* SentryReplayApi.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FA4E5F6A7B8C9D0E1F203142 /* SentryShakeDetector.m in Sources */ = {isa = PBXBuildFile; fileRef = FA2C3D4E5F6A7B8C9D0E1F20 /* SentryShakeDetector.m */; }; + FA3D4E5F6A7B8C9D0E1F2031 /* SentryShakeDetector.h in Headers */ = {isa = PBXBuildFile; fileRef = FA1B2C3D4E5F6A7B8C9D0E1F /* SentryShakeDetector.h */; settings = {ATTRIBUTES = (Public, ); }; }; D8ACE3C82762187200F5A213 /* SentryFileIOTrackerHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = D8ACE3C52762187200F5A213 /* SentryFileIOTrackerHelper.m */; }; D8ACE3CE2762187D00F5A213 /* SentryFileIOTrackerHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CB2762187D00F5A213 /* SentryFileIOTrackerHelper.h */; }; D8B0542E2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D8B0542D2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy */; }; @@ -1090,6 +1092,8 @@ D88D25E92B8E0BAC0073C3D5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; D8A3649A2C91AA3300AC569B /* SentryReplayApi.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryReplayApi.h; path = Public/SentryReplayApi.h; sourceTree = ""; }; D8A3649B2C91AA3300AC569B /* SentryReplayApi.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryReplayApi.m; sourceTree = ""; }; + FA1B2C3D4E5F6A7B8C9D0E1F /* SentryShakeDetector.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryShakeDetector.h; path = Public/SentryShakeDetector.h; sourceTree = ""; }; + FA2C3D4E5F6A7B8C9D0E1F20 /* SentryShakeDetector.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryShakeDetector.m; sourceTree = ""; }; D8ACE3C52762187200F5A213 /* SentryFileIOTrackerHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryFileIOTrackerHelper.m; sourceTree = ""; }; D8ACE3CB2762187D00F5A213 /* SentryFileIOTrackerHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryFileIOTrackerHelper.h; path = include/SentryFileIOTrackerHelper.h; sourceTree = ""; }; D8AE48B12C5786AA0092A2A6 /* SentryLogC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryLogC.h; path = include/SentryLogC.h; sourceTree = ""; }; @@ -2172,6 +2176,8 @@ D82859422C3E753C009A28AA /* SentrySessionReplaySyncC.c */, D8A3649A2C91AA3300AC569B /* SentryReplayApi.h */, D8A3649B2C91AA3300AC569B /* SentryReplayApi.m */, + FA1B2C3D4E5F6A7B8C9D0E1F /* SentryShakeDetector.h */, + FA2C3D4E5F6A7B8C9D0E1F20 /* SentryShakeDetector.m */, ); name = SessionReplay; sourceTree = ""; @@ -2502,6 +2508,7 @@ 63FE713D20DA4C1100CDBAE8 /* SentryAsyncSafeLog.h in Headers */, D43A2A102DD47FB700114724 /* SentryWeakMap.h in Headers */, D8A3649D2C91AA3300AC569B /* SentryReplayApi.h in Headers */, + FA3D4E5F6A7B8C9D0E1F2031 /* SentryShakeDetector.h in Headers */, 63AA769D1EB9C57A00D153DE /* SentryError.h in Headers */, 63FE714F20DA4C1100CDBAE8 /* SentryCrashNSErrorUtil.h in Headers */, 84A7890A2C0E9F6400FF0803 /* SentrySpanInternal+Private.h in Headers */, @@ -3032,6 +3039,7 @@ 9286059729A5098900F96038 /* SentryGeo.m in Sources */, D43B26D82D70A550007747FD /* SentryTraceOrigin.m in Sources */, D8A3649C2C91AA3300AC569B /* SentryReplayApi.m in Sources */, + FA4E5F6A7B8C9D0E1F203142 /* SentryShakeDetector.m in Sources */, 639FCFAD1EBC811400778193 /* SentryUser.m in Sources */, 63FE711920DA4C1000CDBAE8 /* SentryCrashMachineContext.c in Sources */, 63FE711B20DA4C1000CDBAE8 /* SentryCrashString.c in Sources */, diff --git a/Sources/Sentry/Public/Sentry.h b/Sources/Sentry/Public/Sentry.h index f7c91acb9dc..0eeda04cc80 100644 --- a/Sources/Sentry/Public/Sentry.h +++ b/Sources/Sentry/Public/Sentry.h @@ -32,6 +32,7 @@ FOUNDATION_EXPORT const unsigned char SentryVersionString[]; # import # import # import +# import # import # import # import diff --git a/Sources/Sentry/Public/SentryShakeDetector.h b/Sources/Sentry/Public/SentryShakeDetector.h new file mode 100644 index 00000000000..318c4fa1189 --- /dev/null +++ b/Sources/Sentry/Public/SentryShakeDetector.h @@ -0,0 +1,45 @@ +#if __has_include() +# import +#elif __has_include() +# import +#else +# import +#endif + +#if TARGET_OS_IOS + +NS_ASSUME_NONNULL_BEGIN + +/** + * Notification posted when the device detects a shake gesture. + * Subscribe to this notification to be informed when the user shakes the device. + */ +SENTRY_EXTERN NSNotificationName const SentryShakeDetectedNotification; + +/** + * Detects shake gestures by swizzling @c UIWindow 's @c motionEnded:withEvent: method. + * When a shake gesture is detected, posts a @c SentryShakeDetectedNotification notification. + * + * Use @c +enable to start detection and @c +disable to stop it. + * Swizzling is performed at most once regardless of how many times @c +enable is called. + */ +@interface SentryShakeDetector : NSObject + +/** + * Enables shake gesture detection. Swizzles @c UIWindow 's @c motionEnded:withEvent: method + * the first time it is called, and from then on posts @c SentryShakeDetectedNotification + * whenever a shake is detected. + */ ++ (void)enable; + +/** + * Disables shake gesture detection. Does not un-swizzle @c UIWindow ; it only suppresses + * the notification so the overhead is negligible. + */ ++ (void)disable; + +@end + +NS_ASSUME_NONNULL_END + +#endif // TARGET_OS_IOS diff --git a/Sources/Sentry/SentryShakeDetector.m b/Sources/Sentry/SentryShakeDetector.m new file mode 100644 index 00000000000..836f0cff6d4 --- /dev/null +++ b/Sources/Sentry/SentryShakeDetector.m @@ -0,0 +1,78 @@ +#import "SentryShakeDetector.h" +#import +#import + +#if TARGET_OS_IOS + +NSNotificationName const SentryShakeDetectedNotification = @"SentryShakeDetected"; + +static BOOL _shakeDetectionEnabled = NO; +static BOOL _swizzled = NO; +static IMP _originalMotionEndedIMP = NULL; +static NSTimeInterval _lastShakeTimestamp = 0; +static const NSTimeInterval SHAKE_COOLDOWN_SECONDS = 1.0; + +static void +sentry_motionEnded(UIWindow *self, SEL _cmd, UIEventSubtype motion, UIEvent *event) +{ + if (_shakeDetectionEnabled && motion == UIEventSubtypeMotionShake) { + NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; + if (now - _lastShakeTimestamp > SHAKE_COOLDOWN_SECONDS) { + _lastShakeTimestamp = now; + [[NSNotificationCenter defaultCenter] + postNotificationName:SentryShakeDetectedNotification + object:nil]; + } + } + if (_originalMotionEndedIMP) { + ((void (*)(id, SEL, UIEventSubtype, UIEvent *))_originalMotionEndedIMP)( + self, _cmd, motion, event); + } +} + +@implementation SentryShakeDetector + ++ (void)enable +{ + @synchronized(self) { + if (!_swizzled) { + Class windowClass = [UIWindow class]; + SEL sel = @selector(motionEnded:withEvent:); + Method inheritedMethod = class_getInstanceMethod(windowClass, sel); + if (!inheritedMethod) { + return; + } + IMP inheritedIMP = method_getImplementation(inheritedMethod); + const char *types = method_getTypeEncoding(inheritedMethod); + class_addMethod(windowClass, sel, inheritedIMP, types); + Method ownMethod = class_getInstanceMethod(windowClass, sel); + _originalMotionEndedIMP = method_setImplementation(ownMethod, (IMP)sentry_motionEnded); + _swizzled = YES; + } + _shakeDetectionEnabled = YES; + } +} + ++ (void)disable +{ + @synchronized(self) { + _shakeDetectionEnabled = NO; + } +} + +@end + +#else + +NSNotificationName const SentryShakeDetectedNotification = @"SentryShakeDetected"; + +@implementation SentryShakeDetector ++ (void)enable +{ +} ++ (void)disable +{ +} +@end + +#endif diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift index 84e76e28645..4006e3842d5 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift @@ -53,10 +53,15 @@ final class SentryUserFeedbackIntegrationDriver: NSObject { } observeScreenshots() + observeShakeGesture() } deinit { customButton?.removeTarget(self, action: #selector(showForm(sender:)), for: .touchUpInside) + if configuration.useShakeGesture { + SentryShakeDetector.disable() + NotificationCenter.default.removeObserver(self, name: NSNotification.Name("SentryShakeDetected"), object: nil) + } } func showWidget() { @@ -149,6 +154,18 @@ private extension SentryUserFeedbackIntegrationDriver { } } + func observeShakeGesture() { + if configuration.useShakeGesture { + SentryShakeDetector.enable() + NotificationCenter.default.addObserver(self, selector: #selector(handleShakeGesture), name: NSNotification.Name("SentryShakeDetected"), object: nil) + } + } + + @objc func handleShakeGesture() { + guard !displayingForm else { return } + showForm(screenshot: nil) + } + @objc func userCapturedScreenshot() { stopObservingScreenshots() showForm(screenshot: screenshotSource.appScreenshots().first) From fb973ee3f17820055ed46576a838a6898f5f3a90 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 4 Mar 2026 11:27:32 +0100 Subject: [PATCH 02/17] fix(feedback): guard UIKit import inside TARGET_OS_IOS to fix non-UIKit targets Co-Authored-By: Claude Sonnet 4.6 --- Sources/Sentry/SentryShakeDetector.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/SentryShakeDetector.m b/Sources/Sentry/SentryShakeDetector.m index 836f0cff6d4..5f69f220f38 100644 --- a/Sources/Sentry/SentryShakeDetector.m +++ b/Sources/Sentry/SentryShakeDetector.m @@ -1,8 +1,8 @@ #import "SentryShakeDetector.h" -#import #import #if TARGET_OS_IOS +# import NSNotificationName const SentryShakeDetectedNotification = @"SentryShakeDetected"; From cc676febfe5774c4a183be36d2b9035bf7c1a0c2 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 4 Mar 2026 12:47:00 +0100 Subject: [PATCH 03/17] fix(feedback): expose SentryShakeDetector on all platforms; no-op on non-iOS The @interface declaration was wrapped in #if TARGET_OS_IOS, causing a compile error on macOS/tvOS/watchOS where the @implementation in the #else block could not find the interface. The class is now declared on all platforms with the methods documented as no-ops on non-iOS. Co-Authored-By: Claude Sonnet 4.6 --- Sources/Sentry/Public/SentryShakeDetector.h | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/Sources/Sentry/Public/SentryShakeDetector.h b/Sources/Sentry/Public/SentryShakeDetector.h index 318c4fa1189..4dac6dd5ebc 100644 --- a/Sources/Sentry/Public/SentryShakeDetector.h +++ b/Sources/Sentry/Public/SentryShakeDetector.h @@ -6,40 +6,37 @@ # import #endif -#if TARGET_OS_IOS - NS_ASSUME_NONNULL_BEGIN /** - * Notification posted when the device detects a shake gesture. - * Subscribe to this notification to be informed when the user shakes the device. + * Notification posted when the device detects a shake gesture on iOS. + * On non-iOS platforms this notification is never posted. */ SENTRY_EXTERN NSNotificationName const SentryShakeDetectedNotification; /** - * Detects shake gestures by swizzling @c UIWindow 's @c motionEnded:withEvent: method. + * Detects shake gestures by swizzling @c UIWindow 's @c motionEnded:withEvent: method on iOS. * When a shake gesture is detected, posts a @c SentryShakeDetectedNotification notification. * * Use @c +enable to start detection and @c +disable to stop it. * Swizzling is performed at most once regardless of how many times @c +enable is called. + * On non-iOS platforms (macOS, tvOS, watchOS), these methods are no-ops. */ @interface SentryShakeDetector : NSObject /** - * Enables shake gesture detection. Swizzles @c UIWindow 's @c motionEnded:withEvent: method + * Enables shake gesture detection. On iOS, swizzles @c UIWindow 's @c motionEnded:withEvent: * the first time it is called, and from then on posts @c SentryShakeDetectedNotification - * whenever a shake is detected. + * whenever a shake is detected. No-op on non-iOS platforms. */ + (void)enable; /** * Disables shake gesture detection. Does not un-swizzle @c UIWindow ; it only suppresses - * the notification so the overhead is negligible. + * the notification so the overhead is negligible. No-op on non-iOS platforms. */ + (void)disable; @end NS_ASSUME_NONNULL_END - -#endif // TARGET_OS_IOS From 35753773937ceb6be818f0692a31e575075841f6 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 4 Mar 2026 13:23:59 +0100 Subject: [PATCH 04/17] impr(feedback): harden shake detection Use monotonic clock (CACurrentMediaTime) instead of NSDate, atomic_bool for thread-safe enabled flag, bridged notification constant, and unconditional cleanup in deinit. --- CHANGELOG.md | 3 +++ Sources/Sentry/SentryShakeDetector.m | 19 ++++++++++--------- .../SentryUserFeedbackIntegrationDriver.swift | 8 +++----- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9eebd0c988..9d4232de246 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Features +- Show feedback form on device shake (#7579) + - Enable via `config.useShakeGesture = true` in `SentryUserFeedbackConfiguration` + - Uses UIKit's built-in shake detection — no special permissions required - Add watchdog termination hang tracker using RunLoop observer. Can be enabled by setting `options.experimental.enableWatchdogTerminationsV2 = true` (#7464) ### Fixes diff --git a/Sources/Sentry/SentryShakeDetector.m b/Sources/Sentry/SentryShakeDetector.m index 5f69f220f38..9159850332f 100644 --- a/Sources/Sentry/SentryShakeDetector.m +++ b/Sources/Sentry/SentryShakeDetector.m @@ -1,22 +1,25 @@ #import "SentryShakeDetector.h" #import +#import #if TARGET_OS_IOS +# import # import NSNotificationName const SentryShakeDetectedNotification = @"SentryShakeDetected"; -static BOOL _shakeDetectionEnabled = NO; +static atomic_bool _shakeDetectionEnabled = NO; static BOOL _swizzled = NO; static IMP _originalMotionEndedIMP = NULL; -static NSTimeInterval _lastShakeTimestamp = 0; -static const NSTimeInterval SHAKE_COOLDOWN_SECONDS = 1.0; +static CFTimeInterval _lastShakeTimestamp = 0; +static const CFTimeInterval SHAKE_COOLDOWN_SECONDS = 1.0; static void sentry_motionEnded(UIWindow *self, SEL _cmd, UIEventSubtype motion, UIEvent *event) { - if (_shakeDetectionEnabled && motion == UIEventSubtypeMotionShake) { - NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; + if (atomic_load_explicit(&_shakeDetectionEnabled, memory_order_acquire) + && motion == UIEventSubtypeMotionShake) { + CFTimeInterval now = CACurrentMediaTime(); if (now - _lastShakeTimestamp > SHAKE_COOLDOWN_SECONDS) { _lastShakeTimestamp = now; [[NSNotificationCenter defaultCenter] @@ -49,15 +52,13 @@ + (void)enable _originalMotionEndedIMP = method_setImplementation(ownMethod, (IMP)sentry_motionEnded); _swizzled = YES; } - _shakeDetectionEnabled = YES; + atomic_store_explicit(&_shakeDetectionEnabled, YES, memory_order_release); } } + (void)disable { - @synchronized(self) { - _shakeDetectionEnabled = NO; - } + atomic_store_explicit(&_shakeDetectionEnabled, NO, memory_order_release); } @end diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift index 4006e3842d5..a34f087f4cb 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift @@ -58,10 +58,8 @@ final class SentryUserFeedbackIntegrationDriver: NSObject { deinit { customButton?.removeTarget(self, action: #selector(showForm(sender:)), for: .touchUpInside) - if configuration.useShakeGesture { - SentryShakeDetector.disable() - NotificationCenter.default.removeObserver(self, name: NSNotification.Name("SentryShakeDetected"), object: nil) - } + SentryShakeDetector.disable() + NotificationCenter.default.removeObserver(self) } func showWidget() { @@ -157,7 +155,7 @@ private extension SentryUserFeedbackIntegrationDriver { func observeShakeGesture() { if configuration.useShakeGesture { SentryShakeDetector.enable() - NotificationCenter.default.addObserver(self, selector: #selector(handleShakeGesture), name: NSNotification.Name("SentryShakeDetected"), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(handleShakeGesture), name: .SentryShakeDetected, object: nil) } } From 67a100de236eb9fd50be02606044d3970f411a28 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 4 Mar 2026 13:53:59 +0100 Subject: [PATCH 05/17] chore: update public API snapshot for SentryShakeDetector Co-Authored-By: Claude Sonnet 4.6 --- sdk_api.json | 306 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) diff --git a/sdk_api.json b/sdk_api.json index e672212f87c..48ef27684f0 100644 --- a/sdk_api.json +++ b/sdk_api.json @@ -656,6 +656,16 @@ "name": "Sentry.SentrySessionReplayHybridSDK", "printedName": "Sentry.SentrySessionReplayHybridSDK" }, + { + "declAttributes": [ + "Exported" + ], + "declKind": "Import", + "kind": "Import", + "moduleName": "Sentry", + "name": "Sentry.SentryShakeDetector", + "printedName": "Sentry.SentryShakeDetector" + }, { "declAttributes": [ "Exported" @@ -19070,6 +19080,139 @@ "superclassUsr": "c:objc(cs)NSObject", "usr": "c:objc(cs)SentryMechanismContext" }, + { + "children": [ + { + "accessors": [ + { + "accessorKind": "get", + "children": [ + { + "kind": "TypeNominal", + "name": "Name", + "printedName": "Foundation.NSNotification.Name", + "usr": "c:@T@NSNotificationName" + } + ], + "declKind": "Accessor", + "implicit": true, + "isFromExtension": true, + "kind": "Accessor", + "mangledName": "$sSo18NSNotificationNamea6SentryE0C13ShakeDetectedABvgZ", + "moduleName": "Sentry", + "name": "Get", + "printedName": "Get()", + "static": true, + "usr": "s:So18NSNotificationNamea6SentryE0C13ShakeDetectedABvgZ" + } + ], + "children": [ + { + "kind": "TypeNominal", + "name": "Name", + "printedName": "Foundation.NSNotification.Name", + "usr": "c:@T@NSNotificationName" + } + ], + "declAttributes": [ + "Nonisolated" + ], + "declKind": "Var", + "hasStorage": true, + "isFromExtension": true, + "isLet": true, + "kind": "Var", + "moduleName": "Sentry", + "name": "SentryShakeDetected", + "printedName": "SentryShakeDetected", + "static": true, + "usr": "c:@SentryShakeDetectedNotification" + } + ], + "conformances": [ + { + "kind": "Conformance", + "mangledName": "$ss8CopyableP", + "name": "Copyable", + "printedName": "Copyable", + "usr": "s:s8CopyableP" + }, + { + "kind": "Conformance", + "mangledName": "$sSQ", + "name": "Equatable", + "printedName": "Equatable", + "usr": "s:SQ" + }, + { + "kind": "Conformance", + "mangledName": "$ss9EscapableP", + "name": "Escapable", + "printedName": "Escapable", + "usr": "s:s9EscapableP" + }, + { + "kind": "Conformance", + "mangledName": "$sSH", + "name": "Hashable", + "printedName": "Hashable", + "usr": "s:SH" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "kind": "TypeNameAlias", + "name": "RawValue", + "printedName": "Foundation.NSNotification.Name.RawValue" + } + ], + "kind": "TypeWitness", + "name": "RawValue", + "printedName": "RawValue" + } + ], + "kind": "Conformance", + "mangledName": "$sSY", + "name": "RawRepresentable", + "printedName": "RawRepresentable", + "usr": "s:SY" + }, + { + "kind": "Conformance", + "mangledName": "$ss8SendableP", + "name": "Sendable", + "printedName": "Sendable", + "usr": "s:s8SendableP" + } + ], + "declAttributes": [ + "Sendable", + "SynthesizedProtocol", + "SynthesizedProtocol", + "SynthesizedProtocol", + "SynthesizedProtocol", + "SynthesizedProtocol", + "SynthesizedProtocol" + ], + "declKind": "Struct", + "isExternal": true, + "isFromExtension": true, + "kind": "TypeDecl", + "moduleName": "Foundation", + "name": "Name", + "printedName": "Name", + "usr": "c:@T@NSNotificationName" + }, { "children": [ { @@ -52525,6 +52668,169 @@ "superclassUsr": "c:objc(cs)NSObject", "usr": "c:objc(cs)SentrySessionReplayHybridSDK" }, + { + "children": [ + { + "children": [ + { + "kind": "TypeNominal", + "name": "SentryShakeDetector", + "printedName": "Sentry.SentryShakeDetector", + "usr": "c:objc(cs)SentryShakeDetector" + } + ], + "declAttributes": [ + "Dynamic", + "ObjC", + "Override" + ], + "declKind": "Constructor", + "implicit": true, + "init_kind": "Designated", + "kind": "Constructor", + "moduleName": "Sentry", + "name": "init", + "objc_name": "init", + "overriding": true, + "printedName": "init()", + "usr": "c:objc(cs)NSObject(im)init" + }, + { + "children": [ + { + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + } + ], + "kind": "TypeNameAlias", + "name": "Void", + "printedName": "Swift.Void" + } + ], + "declAttributes": [ + "Dynamic", + "ObjC" + ], + "declKind": "Func", + "funcSelfKind": "NonMutating", + "isOpen": true, + "kind": "Function", + "moduleName": "Sentry", + "name": "disable", + "objc_name": "disable", + "printedName": "disable()", + "static": true, + "usr": "c:objc(cs)SentryShakeDetector(cm)disable" + }, + { + "children": [ + { + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + } + ], + "kind": "TypeNameAlias", + "name": "Void", + "printedName": "Swift.Void" + } + ], + "declAttributes": [ + "Dynamic", + "ObjC" + ], + "declKind": "Func", + "funcSelfKind": "NonMutating", + "isOpen": true, + "kind": "Function", + "moduleName": "Sentry", + "name": "enable", + "objc_name": "enable", + "printedName": "enable()", + "static": true, + "usr": "c:objc(cs)SentryShakeDetector(cm)enable" + } + ], + "conformances": [ + { + "kind": "Conformance", + "mangledName": "$ss7CVarArgP", + "name": "CVarArg", + "printedName": "CVarArg", + "usr": "s:s7CVarArgP" + }, + { + "kind": "Conformance", + "mangledName": "$ss8CopyableP", + "name": "Copyable", + "printedName": "Copyable", + "usr": "s:s8CopyableP" + }, + { + "kind": "Conformance", + "mangledName": "$ss28CustomDebugStringConvertibleP", + "name": "CustomDebugStringConvertible", + "printedName": "CustomDebugStringConvertible", + "usr": "s:s28CustomDebugStringConvertibleP" + }, + { + "kind": "Conformance", + "mangledName": "$ss23CustomStringConvertibleP", + "name": "CustomStringConvertible", + "printedName": "CustomStringConvertible", + "usr": "s:s23CustomStringConvertibleP" + }, + { + "kind": "Conformance", + "mangledName": "$sSQ", + "name": "Equatable", + "printedName": "Equatable", + "usr": "s:SQ" + }, + { + "kind": "Conformance", + "mangledName": "$ss9EscapableP", + "name": "Escapable", + "printedName": "Escapable", + "usr": "s:s9EscapableP" + }, + { + "kind": "Conformance", + "mangledName": "$sSH", + "name": "Hashable", + "printedName": "Hashable", + "usr": "s:SH" + }, + { + "kind": "Conformance", + "name": "NSObjectProtocol", + "printedName": "NSObjectProtocol", + "usr": "c:objc(pl)NSObject" + } + ], + "declAttributes": [ + "Dynamic", + "ObjC" + ], + "declKind": "Class", + "inheritsConvenienceInitializers": true, + "isOpen": true, + "kind": "TypeDecl", + "moduleName": "Sentry", + "name": "SentryShakeDetector", + "objc_name": "SentryShakeDetector", + "printedName": "SentryShakeDetector", + "superclassNames": [ + "ObjectiveC.NSObject" + ], + "superclassUsr": "c:objc(cs)NSObject", + "usr": "c:objc(cs)SentryShakeDetector" + }, { "children": [ { From bcda2681a82864b753cde6aca103d4ccb5f55f13 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 4 Mar 2026 14:26:24 +0100 Subject: [PATCH 06/17] test(feedback): add unit tests for SentryShakeDetector --- .../Feedback/SentryShakeDetectorTests.swift | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 Tests/SentryTests/Integrations/Feedback/SentryShakeDetectorTests.swift diff --git a/Tests/SentryTests/Integrations/Feedback/SentryShakeDetectorTests.swift b/Tests/SentryTests/Integrations/Feedback/SentryShakeDetectorTests.swift new file mode 100644 index 00000000000..0213e4ea12c --- /dev/null +++ b/Tests/SentryTests/Integrations/Feedback/SentryShakeDetectorTests.swift @@ -0,0 +1,96 @@ +@testable import Sentry +import XCTest + +#if os(iOS) +import UIKit + +final class SentryShakeDetectorTests: XCTestCase { + + override func tearDown() { + super.tearDown() + SentryShakeDetector.disable() + } + + func testEnable_whenShakeOccurs_shouldPostNotification() { + let expectation = expectation(forNotification: .SentryShakeDetected, object: nil) + + SentryShakeDetector.enable() + + let window = UIWindow() + window.motionEnded(.motionShake, with: nil) + + wait(for: [expectation], timeout: 1.0) + } + + func testDisable_whenShakeOccurs_shouldNotPostNotification() { + SentryShakeDetector.enable() + SentryShakeDetector.disable() + + let expectation = expectation(forNotification: .SentryShakeDetected, object: nil) + expectation.isInverted = true + + let window = UIWindow() + window.motionEnded(.motionShake, with: nil) + + wait(for: [expectation], timeout: 0.5) + } + + func testEnable_whenNonShakeMotion_shouldNotPostNotification() { + SentryShakeDetector.enable() + + let expectation = expectation(forNotification: .SentryShakeDetected, object: nil) + expectation.isInverted = true + + let window = UIWindow() + window.motionEnded(.none, with: nil) + + wait(for: [expectation], timeout: 0.5) + } + + func testEnable_calledMultipleTimes_shouldNotCrash() { + SentryShakeDetector.enable() + SentryShakeDetector.enable() + SentryShakeDetector.enable() + + // Just verify no crash; the swizzle-once guard handles repeated calls + let window = UIWindow() + window.motionEnded(.motionShake, with: nil) + } + + func testDisable_withoutEnable_shouldNotCrash() { + SentryShakeDetector.disable() + } + + func testCooldown_whenShakesTooFast_shouldPostOnlyOnce() { + SentryShakeDetector.enable() + + var notificationCount = 0 + let observer = NotificationCenter.default.addObserver( + forName: .SentryShakeDetected, object: nil, queue: nil + ) { _ in + notificationCount += 1 + } + + let window = UIWindow() + // Rapid shakes within the 1s cooldown + window.motionEnded(.motionShake, with: nil) + window.motionEnded(.motionShake, with: nil) + window.motionEnded(.motionShake, with: nil) + + XCTAssertEqual(notificationCount, 1) + + NotificationCenter.default.removeObserver(observer) + } + + func testOriginalImplementation_shouldStillBeCalled() { + SentryShakeDetector.enable() + + // motionEnded should not crash — the original UIResponder implementation + // is called after our interceptor + let window = UIWindow() + window.motionEnded(.motionShake, with: nil) + window.motionEnded(.remoteControlBeginSeekingBackward, with: nil) + } +} + +#endif From 27ff27e30be6e1fe93ca0abd4af8383c5c2794c0 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 4 Mar 2026 14:40:12 +0100 Subject: [PATCH 07/17] fix(feedback): prevent deinit race on shake detector Track whether this driver instance enabled shake detection and only call disable in deinit if it did. Prevents an old driver's deallocation from disabling shake detection that a new driver already re-enabled during SDK restart. --- .../UserFeedback/SentryUserFeedbackIntegrationDriver.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift index a34f087f4cb..bf01bfec4d0 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift @@ -15,6 +15,7 @@ final class SentryUserFeedbackIntegrationDriver: NSObject { fileprivate let callback: (SentryFeedback) -> Void let screenshotSource: SentryScreenshotSource weak var customButton: UIButton? + private var didEnableShakeDetection = false init(configuration: SentryUserFeedbackConfiguration, screenshotSource: SentryScreenshotSource, callback: @escaping (SentryFeedback) -> Void) { self.configuration = configuration @@ -58,7 +59,9 @@ final class SentryUserFeedbackIntegrationDriver: NSObject { deinit { customButton?.removeTarget(self, action: #selector(showForm(sender:)), for: .touchUpInside) - SentryShakeDetector.disable() + if didEnableShakeDetection { + SentryShakeDetector.disable() + } NotificationCenter.default.removeObserver(self) } @@ -155,6 +158,7 @@ private extension SentryUserFeedbackIntegrationDriver { func observeShakeGesture() { if configuration.useShakeGesture { SentryShakeDetector.enable() + didEnableShakeDetection = true NotificationCenter.default.addObserver(self, selector: #selector(handleShakeGesture), name: .SentryShakeDetected, object: nil) } } From 3e4b59308e797cb552a266df966f3a00270165d6 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 4 Mar 2026 14:59:03 +0100 Subject: [PATCH 08/17] fix(feedback): set up shake detection before SwiftUI early return The init early-returns when there are no connected scenes (SwiftUI apps). Move observeScreenshots and observeShakeGesture calls before the return so shake detection works in SwiftUI apps. --- .../UserFeedback/SentryUserFeedbackIntegrationDriver.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift index bf01bfec4d0..779604caacc 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift @@ -45,6 +45,8 @@ final class SentryUserFeedbackIntegrationDriver: NSObject { * At the time this integration is being installed, if there is no UIApplicationDelegate and no connected UIScene, it is very likely we are in a SwiftUI app, but it's possible we could instead be in a UIKit app that has some nonstandard launch procedure or doesn't call SentrySDK.start in a place we expect/recommend, in which case they will need to manually display the widget when they're ready by calling SentrySDK.feedback.showWidget. */ if UIApplication.shared.connectedScenes.isEmpty && UIApplication.shared.delegate == nil { + observeScreenshots() + observeShakeGesture() return } From 3c24378244f49ceea0c16606f5273e78f81aa2bc Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 5 Mar 2026 14:35:05 +0100 Subject: [PATCH 09/17] ref(feedback): rewrite SentryShakeDetector in Swift Replaces the ObjC implementation with a Swift class while maintaining full ObjC compatibility via @objc annotations. The class name remains SentryShakeDetector in ObjC, preserving compatibility with sentry-react-native. --- Sentry.xcodeproj/project.pbxproj | 8 -- Sources/Sentry/Public/Sentry.h | 1 - Sources/Sentry/Public/SentryShakeDetector.h | 42 -------- Sources/Sentry/SentryShakeDetector.m | 79 --------------- .../UserFeedback/SentryShakeDetector.swift | 98 +++++++++++++++++++ 5 files changed, 98 insertions(+), 130 deletions(-) delete mode 100644 Sources/Sentry/Public/SentryShakeDetector.h delete mode 100644 Sources/Sentry/SentryShakeDetector.m create mode 100644 Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index de9a3ac4822..cb547207e23 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -430,8 +430,6 @@ D88817DA26D72AB800BF2251 /* SentryTraceContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D88817D926D72AB800BF2251 /* SentryTraceContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; D8A3649C2C91AA3300AC569B /* SentryReplayApi.m in Sources */ = {isa = PBXBuildFile; fileRef = D8A3649B2C91AA3300AC569B /* SentryReplayApi.m */; }; D8A3649D2C91AA3300AC569B /* SentryReplayApi.h in Headers */ = {isa = PBXBuildFile; fileRef = D8A3649A2C91AA3300AC569B /* SentryReplayApi.h */; settings = {ATTRIBUTES = (Public, ); }; }; - FA4E5F6A7B8C9D0E1F203142 /* SentryShakeDetector.m in Sources */ = {isa = PBXBuildFile; fileRef = FA2C3D4E5F6A7B8C9D0E1F20 /* SentryShakeDetector.m */; }; - FA3D4E5F6A7B8C9D0E1F2031 /* SentryShakeDetector.h in Headers */ = {isa = PBXBuildFile; fileRef = FA1B2C3D4E5F6A7B8C9D0E1F /* SentryShakeDetector.h */; settings = {ATTRIBUTES = (Public, ); }; }; D8ACE3C82762187200F5A213 /* SentryFileIOTrackerHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = D8ACE3C52762187200F5A213 /* SentryFileIOTrackerHelper.m */; }; D8ACE3CE2762187D00F5A213 /* SentryFileIOTrackerHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CB2762187D00F5A213 /* SentryFileIOTrackerHelper.h */; }; D8B0542E2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D8B0542D2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy */; }; @@ -1092,8 +1090,6 @@ D88D25E92B8E0BAC0073C3D5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; D8A3649A2C91AA3300AC569B /* SentryReplayApi.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryReplayApi.h; path = Public/SentryReplayApi.h; sourceTree = ""; }; D8A3649B2C91AA3300AC569B /* SentryReplayApi.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryReplayApi.m; sourceTree = ""; }; - FA1B2C3D4E5F6A7B8C9D0E1F /* SentryShakeDetector.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryShakeDetector.h; path = Public/SentryShakeDetector.h; sourceTree = ""; }; - FA2C3D4E5F6A7B8C9D0E1F20 /* SentryShakeDetector.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryShakeDetector.m; sourceTree = ""; }; D8ACE3C52762187200F5A213 /* SentryFileIOTrackerHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryFileIOTrackerHelper.m; sourceTree = ""; }; D8ACE3CB2762187D00F5A213 /* SentryFileIOTrackerHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryFileIOTrackerHelper.h; path = include/SentryFileIOTrackerHelper.h; sourceTree = ""; }; D8AE48B12C5786AA0092A2A6 /* SentryLogC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryLogC.h; path = include/SentryLogC.h; sourceTree = ""; }; @@ -2176,8 +2172,6 @@ D82859422C3E753C009A28AA /* SentrySessionReplaySyncC.c */, D8A3649A2C91AA3300AC569B /* SentryReplayApi.h */, D8A3649B2C91AA3300AC569B /* SentryReplayApi.m */, - FA1B2C3D4E5F6A7B8C9D0E1F /* SentryShakeDetector.h */, - FA2C3D4E5F6A7B8C9D0E1F20 /* SentryShakeDetector.m */, ); name = SessionReplay; sourceTree = ""; @@ -2508,7 +2502,6 @@ 63FE713D20DA4C1100CDBAE8 /* SentryAsyncSafeLog.h in Headers */, D43A2A102DD47FB700114724 /* SentryWeakMap.h in Headers */, D8A3649D2C91AA3300AC569B /* SentryReplayApi.h in Headers */, - FA3D4E5F6A7B8C9D0E1F2031 /* SentryShakeDetector.h in Headers */, 63AA769D1EB9C57A00D153DE /* SentryError.h in Headers */, 63FE714F20DA4C1100CDBAE8 /* SentryCrashNSErrorUtil.h in Headers */, 84A7890A2C0E9F6400FF0803 /* SentrySpanInternal+Private.h in Headers */, @@ -3039,7 +3032,6 @@ 9286059729A5098900F96038 /* SentryGeo.m in Sources */, D43B26D82D70A550007747FD /* SentryTraceOrigin.m in Sources */, D8A3649C2C91AA3300AC569B /* SentryReplayApi.m in Sources */, - FA4E5F6A7B8C9D0E1F203142 /* SentryShakeDetector.m in Sources */, 639FCFAD1EBC811400778193 /* SentryUser.m in Sources */, 63FE711920DA4C1000CDBAE8 /* SentryCrashMachineContext.c in Sources */, 63FE711B20DA4C1000CDBAE8 /* SentryCrashString.c in Sources */, diff --git a/Sources/Sentry/Public/Sentry.h b/Sources/Sentry/Public/Sentry.h index 0eeda04cc80..f7c91acb9dc 100644 --- a/Sources/Sentry/Public/Sentry.h +++ b/Sources/Sentry/Public/Sentry.h @@ -32,7 +32,6 @@ FOUNDATION_EXPORT const unsigned char SentryVersionString[]; # import # import # import -# import # import # import # import diff --git a/Sources/Sentry/Public/SentryShakeDetector.h b/Sources/Sentry/Public/SentryShakeDetector.h deleted file mode 100644 index 4dac6dd5ebc..00000000000 --- a/Sources/Sentry/Public/SentryShakeDetector.h +++ /dev/null @@ -1,42 +0,0 @@ -#if __has_include() -# import -#elif __has_include() -# import -#else -# import -#endif - -NS_ASSUME_NONNULL_BEGIN - -/** - * Notification posted when the device detects a shake gesture on iOS. - * On non-iOS platforms this notification is never posted. - */ -SENTRY_EXTERN NSNotificationName const SentryShakeDetectedNotification; - -/** - * Detects shake gestures by swizzling @c UIWindow 's @c motionEnded:withEvent: method on iOS. - * When a shake gesture is detected, posts a @c SentryShakeDetectedNotification notification. - * - * Use @c +enable to start detection and @c +disable to stop it. - * Swizzling is performed at most once regardless of how many times @c +enable is called. - * On non-iOS platforms (macOS, tvOS, watchOS), these methods are no-ops. - */ -@interface SentryShakeDetector : NSObject - -/** - * Enables shake gesture detection. On iOS, swizzles @c UIWindow 's @c motionEnded:withEvent: - * the first time it is called, and from then on posts @c SentryShakeDetectedNotification - * whenever a shake is detected. No-op on non-iOS platforms. - */ -+ (void)enable; - -/** - * Disables shake gesture detection. Does not un-swizzle @c UIWindow ; it only suppresses - * the notification so the overhead is negligible. No-op on non-iOS platforms. - */ -+ (void)disable; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryShakeDetector.m b/Sources/Sentry/SentryShakeDetector.m deleted file mode 100644 index 9159850332f..00000000000 --- a/Sources/Sentry/SentryShakeDetector.m +++ /dev/null @@ -1,79 +0,0 @@ -#import "SentryShakeDetector.h" -#import -#import - -#if TARGET_OS_IOS -# import -# import - -NSNotificationName const SentryShakeDetectedNotification = @"SentryShakeDetected"; - -static atomic_bool _shakeDetectionEnabled = NO; -static BOOL _swizzled = NO; -static IMP _originalMotionEndedIMP = NULL; -static CFTimeInterval _lastShakeTimestamp = 0; -static const CFTimeInterval SHAKE_COOLDOWN_SECONDS = 1.0; - -static void -sentry_motionEnded(UIWindow *self, SEL _cmd, UIEventSubtype motion, UIEvent *event) -{ - if (atomic_load_explicit(&_shakeDetectionEnabled, memory_order_acquire) - && motion == UIEventSubtypeMotionShake) { - CFTimeInterval now = CACurrentMediaTime(); - if (now - _lastShakeTimestamp > SHAKE_COOLDOWN_SECONDS) { - _lastShakeTimestamp = now; - [[NSNotificationCenter defaultCenter] - postNotificationName:SentryShakeDetectedNotification - object:nil]; - } - } - if (_originalMotionEndedIMP) { - ((void (*)(id, SEL, UIEventSubtype, UIEvent *))_originalMotionEndedIMP)( - self, _cmd, motion, event); - } -} - -@implementation SentryShakeDetector - -+ (void)enable -{ - @synchronized(self) { - if (!_swizzled) { - Class windowClass = [UIWindow class]; - SEL sel = @selector(motionEnded:withEvent:); - Method inheritedMethod = class_getInstanceMethod(windowClass, sel); - if (!inheritedMethod) { - return; - } - IMP inheritedIMP = method_getImplementation(inheritedMethod); - const char *types = method_getTypeEncoding(inheritedMethod); - class_addMethod(windowClass, sel, inheritedIMP, types); - Method ownMethod = class_getInstanceMethod(windowClass, sel); - _originalMotionEndedIMP = method_setImplementation(ownMethod, (IMP)sentry_motionEnded); - _swizzled = YES; - } - atomic_store_explicit(&_shakeDetectionEnabled, YES, memory_order_release); - } -} - -+ (void)disable -{ - atomic_store_explicit(&_shakeDetectionEnabled, NO, memory_order_release); -} - -@end - -#else - -NSNotificationName const SentryShakeDetectedNotification = @"SentryShakeDetected"; - -@implementation SentryShakeDetector -+ (void)enable -{ -} -+ (void)disable -{ -} -@end - -#endif diff --git a/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift b/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift new file mode 100644 index 00000000000..8b5b31f4ab2 --- /dev/null +++ b/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift @@ -0,0 +1,98 @@ +import Foundation +#if os(iOS) && !SENTRY_NO_UI_FRAMEWORK +import ObjectiveC +import QuartzCore +import UIKit +#endif + +/// Extension providing the Sentry shake detection notification name. +public extension NSNotification.Name { + /// Notification posted when the device detects a shake gesture on iOS/iPadOS. + /// On non-iOS platforms this notification is never posted. + static let SentryShakeDetected = NSNotification.Name("SentryShakeDetected") +} + +/// Detects shake gestures by swizzling `UIWindow.motionEnded(_:with:)` on iOS/iPadOS. +/// When a shake gesture is detected, posts a `.SentryShakeDetected` notification. +/// +/// Use `enable()` to start detection and `disable()` to stop it. +/// Swizzling is performed at most once regardless of how many times `enable()` is called. +/// On non-iOS platforms (macOS, tvOS, watchOS), these methods are no-ops. +@objc(SentryShakeDetector) +@objcMembers +public class SentryShakeDetector: NSObject { + + /// The notification name posted on shake, exposed for ObjC consumers. + /// In Swift, prefer using `.SentryShakeDetected` on `NSNotification.Name` directly. + @objc public static let shakeDetectedNotification = NSNotification.Name.SentryShakeDetected + +#if os(iOS) && !SENTRY_NO_UI_FRAMEWORK + // Both motionEnded (main thread) and enable/disable (main thread in practice) + // access this flag. UIKit's motionEnded is always dispatched on the main thread, + // and the SDK calls enable/disable from main-thread integration lifecycle. + private static var enabled = false + + private static var swizzled = false + private static var originalIMP: IMP? + private static var lastShakeTimestamp: CFTimeInterval = 0 + private static let cooldownSeconds: CFTimeInterval = 1.0 + private static let lock = NSLock() + + /// Enables shake gesture detection. On iOS/iPadOS, swizzles `UIWindow.motionEnded(_:with:)` + /// the first time it is called, and from then on posts `.SentryShakeDetected` + /// whenever a shake is detected. No-op on non-iOS platforms. + public static func enable() { + lock.lock() + defer { lock.unlock() } + + if !swizzled { + let windowClass: AnyClass = UIWindow.self + let selector = #selector(UIResponder.motionEnded(_:with:)) + + guard let inheritedMethod = class_getInstanceMethod(windowClass, selector) else { + return + } + + let inheritedIMP = method_getImplementation(inheritedMethod) + let types = method_getTypeEncoding(inheritedMethod) + class_addMethod(windowClass, selector, inheritedIMP, types) + + guard let ownMethod = class_getInstanceMethod(windowClass, selector) else { + return + } + + let replacementIMP = imp_implementationWithBlock({ (self: UIWindow, motion: UIEvent.EventSubtype, event: UIEvent?) in + if SentryShakeDetector.enabled && motion == .motionShake { + let now = CACurrentMediaTime() + if now - SentryShakeDetector.lastShakeTimestamp > SentryShakeDetector.cooldownSeconds { + SentryShakeDetector.lastShakeTimestamp = now + NotificationCenter.default.post(name: .SentryShakeDetected, object: nil) + } + } + + if let original = SentryShakeDetector.originalIMP { + typealias MotionEndedFunc = @convention(c) (Any, Selector, UIEvent.EventSubtype, UIEvent?) -> Void + let originalFunc = unsafeBitCast(original, to: MotionEndedFunc.self) + originalFunc(self, selector, motion, event) + } + } as @convention(block) (UIWindow, UIEvent.EventSubtype, UIEvent?) -> Void) + + originalIMP = method_setImplementation(ownMethod, replacementIMP) + swizzled = true + } + + enabled = true + } + + /// Disables shake gesture detection. Does not un-swizzle `UIWindow`; it only suppresses + /// the notification so the overhead is negligible. No-op on non-iOS platforms. + public static func disable() { + enabled = false + } +#else + /// No-op on non-iOS platforms. + @objc public static func enable() {} + /// No-op on non-iOS platforms. + @objc public static func disable() {} +#endif +} From 0f86f9fe14146d83a638e53a2452f7fec9164a66 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 5 Mar 2026 14:41:05 +0100 Subject: [PATCH 10/17] Update changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfe603db489..cf85637f7d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,17 @@ # Changelog -## 9.6.0 +## Unreleased ### Features - Show feedback form on device shake (#7579) - Enable via `config.useShakeGesture = true` in `SentryUserFeedbackConfiguration` - Uses UIKit's built-in shake detection — no special permissions required + +## 9.6.0 + +### Features + - Add watchdog termination hang tracker using RunLoop observer. Can be enabled by setting `options.experimental.enableWatchdogTerminationsV2 = true` (#7464) ### Fixes From 6a17a682046ba12dcf3250c8fe6d6bd1fd6d1982 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 5 Mar 2026 14:58:47 +0100 Subject: [PATCH 11/17] chore: update API snapshot for Swift shake detector Make SentryShakeDetector an open class to match the ObjC API and regenerate sdk_api.json with Xcode 16.4. --- .../UserFeedback/SentryShakeDetector.swift | 2 +- sdk_api.json | 122 +++++++++++------- 2 files changed, 79 insertions(+), 45 deletions(-) diff --git a/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift b/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift index 8b5b31f4ab2..f78ddc9862a 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift @@ -20,7 +20,7 @@ public extension NSNotification.Name { /// On non-iOS platforms (macOS, tvOS, watchOS), these methods are no-ops. @objc(SentryShakeDetector) @objcMembers -public class SentryShakeDetector: NSObject { +open class SentryShakeDetector: NSObject { /// The notification name posted on shake, exposed for ObjC consumers. /// In Swift, prefer using `.SentryShakeDetected` on `NSNotification.Name` directly. diff --git a/sdk_api.json b/sdk_api.json index 48ef27684f0..bf84bcd70f6 100644 --- a/sdk_api.json +++ b/sdk_api.json @@ -356,6 +356,13 @@ "name": "PDFKit", "printedName": "PDFKit" }, + { + "declKind": "Import", + "kind": "Import", + "moduleName": "Sentry", + "name": "QuartzCore", + "printedName": "QuartzCore" + }, { "declAttributes": [ "Exported" @@ -656,16 +663,6 @@ "name": "Sentry.SentrySessionReplayHybridSDK", "printedName": "Sentry.SentrySessionReplayHybridSDK" }, - { - "declAttributes": [ - "Exported" - ], - "declKind": "Import", - "kind": "Import", - "moduleName": "Sentry", - "name": "Sentry.SentryShakeDetector", - "printedName": "Sentry.SentryShakeDetector" - }, { "declAttributes": [ "Exported" @@ -19115,18 +19112,19 @@ } ], "declAttributes": [ - "Nonisolated" + "HasStorage" ], "declKind": "Var", "hasStorage": true, "isFromExtension": true, "isLet": true, "kind": "Var", + "mangledName": "$sSo18NSNotificationNamea6SentryE0C13ShakeDetectedABvpZ", "moduleName": "Sentry", "name": "SentryShakeDetected", "printedName": "SentryShakeDetected", "static": true, - "usr": "c:@SentryShakeDetectedNotification" + "usr": "s:So18NSNotificationNamea6SentryE0C13ShakeDetectedABvpZ" } ], "conformances": [ @@ -52676,7 +52674,7 @@ "kind": "TypeNominal", "name": "SentryShakeDetector", "printedName": "Sentry.SentryShakeDetector", - "usr": "c:objc(cs)SentryShakeDetector" + "usr": "c:@M@Sentry@objc(cs)SentryShakeDetector" } ], "declAttributes": [ @@ -52685,75 +52683,110 @@ "Override" ], "declKind": "Constructor", - "implicit": true, "init_kind": "Designated", "kind": "Constructor", + "mangledName": "$s6Sentry0A13ShakeDetectorCACycfc", "moduleName": "Sentry", "name": "init", "objc_name": "init", "overriding": true, "printedName": "init()", - "usr": "c:objc(cs)NSObject(im)init" + "usr": "c:@M@Sentry@objc(cs)SentryShakeDetector(im)init" }, { "children": [ { - "children": [ - { - "kind": "TypeNominal", - "name": "Void", - "printedName": "()" - } - ], - "kind": "TypeNameAlias", + "kind": "TypeNominal", "name": "Void", - "printedName": "Swift.Void" + "printedName": "()" } ], "declAttributes": [ - "Dynamic", + "Final", "ObjC" ], "declKind": "Func", "funcSelfKind": "NonMutating", - "isOpen": true, "kind": "Function", + "mangledName": "$s6Sentry0A13ShakeDetectorC7disableyyFZ", "moduleName": "Sentry", "name": "disable", - "objc_name": "disable", "printedName": "disable()", "static": true, - "usr": "c:objc(cs)SentryShakeDetector(cm)disable" + "usr": "c:@M@Sentry@objc(cs)SentryShakeDetector(cm)disable" }, { "children": [ { - "children": [ - { - "kind": "TypeNominal", - "name": "Void", - "printedName": "()" - } - ], - "kind": "TypeNameAlias", + "kind": "TypeNominal", "name": "Void", - "printedName": "Swift.Void" + "printedName": "()" } ], "declAttributes": [ - "Dynamic", + "Final", "ObjC" ], "declKind": "Func", "funcSelfKind": "NonMutating", - "isOpen": true, "kind": "Function", + "mangledName": "$s6Sentry0A13ShakeDetectorC6enableyyFZ", "moduleName": "Sentry", "name": "enable", - "objc_name": "enable", "printedName": "enable()", "static": true, - "usr": "c:objc(cs)SentryShakeDetector(cm)enable" + "usr": "c:@M@Sentry@objc(cs)SentryShakeDetector(cm)enable" + }, + { + "accessors": [ + { + "accessorKind": "get", + "children": [ + { + "kind": "TypeNominal", + "name": "Name", + "printedName": "Foundation.NSNotification.Name", + "usr": "c:@T@NSNotificationName" + } + ], + "declAttributes": [ + "Final", + "ObjC" + ], + "declKind": "Accessor", + "implicit": true, + "kind": "Accessor", + "mangledName": "$s6Sentry0A13ShakeDetectorC25shakeDetectedNotificationSo18NSNotificationNameavgZ", + "moduleName": "Sentry", + "name": "Get", + "printedName": "Get()", + "static": true, + "usr": "c:@M@Sentry@objc(cs)SentryShakeDetector(cm)shakeDetectedNotification" + } + ], + "children": [ + { + "kind": "TypeNominal", + "name": "Name", + "printedName": "Foundation.NSNotification.Name", + "usr": "c:@T@NSNotificationName" + } + ], + "declAttributes": [ + "Final", + "HasStorage", + "ObjC" + ], + "declKind": "Var", + "hasStorage": true, + "isLet": true, + "kind": "Var", + "mangledName": "$s6Sentry0A13ShakeDetectorC25shakeDetectedNotificationSo18NSNotificationNameavpZ", + "moduleName": "Sentry", + "name": "shakeDetectedNotification", + "printedName": "shakeDetectedNotification", + "static": true, + "usr": "c:@M@Sentry@objc(cs)SentryShakeDetector(cpy)shakeDetectedNotification" } ], "conformances": [ @@ -52814,13 +52847,14 @@ } ], "declAttributes": [ - "Dynamic", - "ObjC" + "ObjC", + "ObjCMembers" ], "declKind": "Class", "inheritsConvenienceInitializers": true, "isOpen": true, "kind": "TypeDecl", + "mangledName": "$s6Sentry0A13ShakeDetectorC", "moduleName": "Sentry", "name": "SentryShakeDetector", "objc_name": "SentryShakeDetector", @@ -52829,7 +52863,7 @@ "ObjectiveC.NSObject" ], "superclassUsr": "c:objc(cs)NSObject", - "usr": "c:objc(cs)SentryShakeDetector" + "usr": "c:@M@Sentry@objc(cs)SentryShakeDetector" }, { "children": [ From f0cd795fc1155377b6012072e6f41aed0a497366 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 5 Mar 2026 15:02:18 +0100 Subject: [PATCH 12/17] ref(feedback): make SentryShakeDetector final The class is a static utility not meant to be subclassed. Regenerate API snapshot accordingly. --- .../Swift/Integrations/UserFeedback/SentryShakeDetector.swift | 2 +- sdk_api.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift b/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift index f78ddc9862a..18425ed8fe8 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift @@ -20,7 +20,7 @@ public extension NSNotification.Name { /// On non-iOS platforms (macOS, tvOS, watchOS), these methods are no-ops. @objc(SentryShakeDetector) @objcMembers -open class SentryShakeDetector: NSObject { +public final class SentryShakeDetector: NSObject { /// The notification name posted on shake, exposed for ObjC consumers. /// In Swift, prefer using `.SentryShakeDetected` on `NSNotification.Name` directly. diff --git a/sdk_api.json b/sdk_api.json index bf84bcd70f6..40ef32f7119 100644 --- a/sdk_api.json +++ b/sdk_api.json @@ -52847,12 +52847,12 @@ } ], "declAttributes": [ + "Final", "ObjC", "ObjCMembers" ], "declKind": "Class", "inheritsConvenienceInitializers": true, - "isOpen": true, "kind": "TypeDecl", "mangledName": "$s6Sentry0A13ShakeDetectorC", "moduleName": "Sentry", From e47ec7be3f6d661bf6921324255813b78de1ab09 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 9 Mar 2026 09:49:18 +0100 Subject: [PATCH 13/17] ref(feedback): simplify deinit shake cleanup Always call SentryShakeDetector.disable() in deinit instead of tracking whether this instance enabled it. --- .../UserFeedback/SentryUserFeedbackIntegrationDriver.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift index 779604caacc..0a221cc84a4 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift @@ -15,7 +15,6 @@ final class SentryUserFeedbackIntegrationDriver: NSObject { fileprivate let callback: (SentryFeedback) -> Void let screenshotSource: SentryScreenshotSource weak var customButton: UIButton? - private var didEnableShakeDetection = false init(configuration: SentryUserFeedbackConfiguration, screenshotSource: SentryScreenshotSource, callback: @escaping (SentryFeedback) -> Void) { self.configuration = configuration @@ -61,9 +60,7 @@ final class SentryUserFeedbackIntegrationDriver: NSObject { deinit { customButton?.removeTarget(self, action: #selector(showForm(sender:)), for: .touchUpInside) - if didEnableShakeDetection { - SentryShakeDetector.disable() - } + SentryShakeDetector.disable() NotificationCenter.default.removeObserver(self) } @@ -160,7 +157,6 @@ private extension SentryUserFeedbackIntegrationDriver { func observeShakeGesture() { if configuration.useShakeGesture { SentryShakeDetector.enable() - didEnableShakeDetection = true NotificationCenter.default.addObserver(self, selector: #selector(handleShakeGesture), name: .SentryShakeDetected, object: nil) } } From 81be8915fdf15a54f60777d15ed8d9e09820aea8 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 9 Mar 2026 16:34:25 +0100 Subject: [PATCH 14/17] impr(feedback): add debug logging to shake detection Add SDK debug logs to swizzling steps in SentryShakeDetector and to shake gesture handling in the integration driver. --- .../UserFeedback/SentryShakeDetector.swift | 5 +++++ .../SentryUserFeedbackIntegrationDriver.swift | 13 +++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift b/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift index 18425ed8fe8..8058c1bc807 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift @@ -50,6 +50,7 @@ public final class SentryShakeDetector: NSObject { let selector = #selector(UIResponder.motionEnded(_:with:)) guard let inheritedMethod = class_getInstanceMethod(windowClass, selector) else { + SentrySDKLog.debug("Shake detector: could not find motionEnded(_:with:) on UIWindow") return } @@ -58,6 +59,7 @@ public final class SentryShakeDetector: NSObject { class_addMethod(windowClass, selector, inheritedIMP, types) guard let ownMethod = class_getInstanceMethod(windowClass, selector) else { + SentrySDKLog.debug("Shake detector: could not add motionEnded(_:with:) to UIWindow") return } @@ -79,15 +81,18 @@ public final class SentryShakeDetector: NSObject { originalIMP = method_setImplementation(ownMethod, replacementIMP) swizzled = true + SentrySDKLog.debug("Shake detector: swizzled UIWindow.motionEnded(_:with:)") } enabled = true + SentrySDKLog.debug("Shake detector: enabled") } /// Disables shake gesture detection. Does not un-swizzle `UIWindow`; it only suppresses /// the notification so the overhead is negligible. No-op on non-iOS platforms. public static func disable() { enabled = false + SentrySDKLog.debug("Shake detector: disabled") } #else /// No-op on non-iOS platforms. diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift index 0a221cc84a4..7f0e8005393 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift @@ -155,14 +155,19 @@ private extension SentryUserFeedbackIntegrationDriver { } func observeShakeGesture() { - if configuration.useShakeGesture { - SentryShakeDetector.enable() - NotificationCenter.default.addObserver(self, selector: #selector(handleShakeGesture), name: .SentryShakeDetected, object: nil) + guard configuration.useShakeGesture else { + SentrySDKLog.debug("Shake gesture detection is disabled in configuration") + return } + SentryShakeDetector.enable() + NotificationCenter.default.addObserver(self, selector: #selector(handleShakeGesture), name: .SentryShakeDetected, object: nil) } @objc func handleShakeGesture() { - guard !displayingForm else { return } + guard !displayingForm else { + SentrySDKLog.debug("Shake gesture ignored — feedback form is already displayed") + return + } showForm(screenshot: nil) } From 1ebfe2f88f62fd65c253ba87de2bf30f71df1e52 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 9 Mar 2026 16:41:23 +0100 Subject: [PATCH 15/17] fix(feedback): prevent displayingForm stuck when presenter is nil Guard showForm early if presenter is nil so displayingForm does not permanently block future shake and screenshot triggers. --- .../UserFeedback/SentryUserFeedbackIntegrationDriver.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift index 7f0e8005393..bf0e64833c0 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift @@ -120,11 +120,15 @@ extension SentryUserFeedbackIntegrationDriver: UIAdaptivePresentationControllerD @available(iOSApplicationExtension, unavailable) private extension SentryUserFeedbackIntegrationDriver { func showForm(screenshot: UIImage?) { + guard let presenter = presenter else { + SentrySDKLog.debug("Cannot show feedback form — no presenter available") + return + } let form = SentryUserFeedbackFormController(config: configuration, delegate: self, screenshot: screenshot) form.presentationController?.delegate = self widget?.rootVC.setWidget(visible: false, animated: configuration.animations) displayingForm = true - presenter?.present(form, animated: configuration.animations) { + presenter.present(form, animated: configuration.animations) { self.configuration.onFormOpen?() } } From 725eba9559befce1b986e47b7549aefb359d267a Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 9 Mar 2026 17:17:47 +0100 Subject: [PATCH 16/17] Emit a warning log when Shake detector could not add motionEnded to UIWindow Co-authored-by: Philip Niedertscheider --- .../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 8058c1bc807..a58746df8c4 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift @@ -59,7 +59,7 @@ public final class SentryShakeDetector: NSObject { class_addMethod(windowClass, selector, inheritedIMP, types) guard let ownMethod = class_getInstanceMethod(windowClass, selector) else { - SentrySDKLog.debug("Shake detector: could not add motionEnded(_:with:) to UIWindow") + SentrySDKLog.warning("Shake detector: could not add motionEnded(_:with:) to UIWindow") return } From 1a04fd852de81a0bd759e703eebedc9fe5fb45b8 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 9 Mar 2026 17:38:51 +0100 Subject: [PATCH 17/17] fix(feedback): remove screenshot observer from SwiftUI early-return The one-shot screenshot observer is wasted in the early-return path where no presenter exists yet. Keep only the persistent shake observer there. --- .../UserFeedback/SentryUserFeedbackIntegrationDriver.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift index bf0e64833c0..7a4953ffc40 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift @@ -44,7 +44,6 @@ final class SentryUserFeedbackIntegrationDriver: NSObject { * At the time this integration is being installed, if there is no UIApplicationDelegate and no connected UIScene, it is very likely we are in a SwiftUI app, but it's possible we could instead be in a UIKit app that has some nonstandard launch procedure or doesn't call SentrySDK.start in a place we expect/recommend, in which case they will need to manually display the widget when they're ready by calling SentrySDK.feedback.showWidget. */ if UIApplication.shared.connectedScenes.isEmpty && UIApplication.shared.delegate == nil { - observeScreenshots() observeShakeGesture() return }