Skip to content

Commit 1e00dfe

Browse files
antonisclaude
andcommitted
feat(feedback): add runtime toggle for shake-to-report
Add Sentry.enableFeedbackOnShake() and Sentry.disableFeedbackOnShake() for runtime control of shake gesture detection, matching the React Native API. The integration now always registers lifecycle callbacks and checks the useShakeGesture flag dynamically on each activity transition. When disabled, there is zero overhead — no sensor or thread allocation. Sensor/thread initialization is deferred to first use. The shake callback also checks the flag before showing the dialog, so disabling takes effect immediately for new shakes even before the next activity transition. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 23b2680 commit 1e00dfe

File tree

5 files changed

+69
-15
lines changed

5 files changed

+69
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
### Features
1010

11+
- Add `Sentry.enableFeedbackOnShake()` and `Sentry.disableFeedbackOnShake()` for runtime control of shake-to-report ([#5232](https://github.com/getsentry/sentry-java/pull/5232))
1112
- Add configurable `IScopesStorageFactory` to `SentryOptions` for providing a custom `IScopesStorage`, e.g. when the default `ThreadLocal`-backed storage is incompatible with non-pinning thread models ([#5199](https://github.com/getsentry/sentry-java/pull/5199))
1213
- Android: Add `beforeErrorSampling` callback to Session Replay ([#5214](https://github.com/getsentry/sentry-java/pull/5214))
1314
- Allows filtering which errors trigger replay capture before the `onErrorSampleRate` is checked

sentry-android-core/src/main/java/io/sentry/android/core/FeedbackShakeIntegration.java

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,17 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions
4444
: null,
4545
"SentryAndroidOptions is required");
4646

47-
if (!this.options.getFeedbackOptions().isUseShakeGesture()) {
48-
return;
49-
}
50-
51-
shakeDetector.init(application, options.getLogger());
52-
5347
addIntegrationToSdkVersion("FeedbackShake");
5448
application.registerActivityLifecycleCallbacks(this);
5549
options.getLogger().log(SentryLevel.DEBUG, "FeedbackShakeIntegration installed.");
5650

57-
// In case of a deferred init, hook into any already-resumed activity
58-
final @Nullable Activity activity = CurrentActivityHolder.getInstance().getActivity();
59-
if (activity != null) {
60-
currentActivityRef = new WeakReference<>(activity);
61-
startShakeDetection(activity);
51+
if (this.options.getFeedbackOptions().isUseShakeGesture()) {
52+
// In case of a deferred init, hook into any already-resumed activity
53+
final @Nullable Activity activity = CurrentActivityHolder.getInstance().getActivity();
54+
if (activity != null) {
55+
currentActivityRef = new WeakReference<>(activity);
56+
startShakeDetection(activity);
57+
}
6258
}
6359
}
6460

@@ -92,7 +88,12 @@ public void onActivityResumed(final @NotNull Activity activity) {
9288
previousOnFormClose = null;
9389
}
9490
currentActivityRef = new WeakReference<>(activity);
95-
startShakeDetection(activity);
91+
// Check dynamically so the flag can be toggled at runtime
92+
if (options != null && options.getFeedbackOptions().isUseShakeGesture()) {
93+
startShakeDetection(activity);
94+
} else {
95+
stopShakeDetection();
96+
}
9697
}
9798

9899
@Override
@@ -146,6 +147,8 @@ private void startShakeDetection(final @NotNull Activity activity) {
146147
if (options == null) {
147148
return;
148149
}
150+
// Initialize sensor and thread if not already done (idempotent)
151+
shakeDetector.init(application, options.getLogger());
149152
// Stop any existing detection (e.g. when transitioning between activities)
150153
stopShakeDetection();
151154
shakeDetector.start(
@@ -156,6 +159,7 @@ private void startShakeDetection(final @NotNull Activity activity) {
156159
final Boolean inBackground = AppState.getInstance().isInBackground();
157160
if (active != null
158161
&& options != null
162+
&& options.getFeedbackOptions().isUseShakeGesture()
159163
&& !isDialogShowing
160164
&& !Boolean.TRUE.equals(inBackground)) {
161165
active.runOnUiThread(

sentry-android-core/src/test/java/io/sentry/android/core/FeedbackShakeIntegrationTest.kt

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import kotlin.test.Test
1010
import org.junit.runner.RunWith
1111
import org.mockito.kotlin.any
1212
import org.mockito.kotlin.mock
13-
import org.mockito.kotlin.never
1413
import org.mockito.kotlin.verify
1514
import org.mockito.kotlin.whenever
1615

@@ -50,11 +49,11 @@ class FeedbackShakeIntegrationTest {
5049
}
5150

5251
@Test
53-
fun `when useShakeGesture is disabled does not register activity lifecycle callbacks`() {
52+
fun `when useShakeGesture is disabled still registers activity lifecycle callbacks for runtime toggle`() {
5453
val sut = fixture.getSut(useShakeGesture = false)
5554
sut.register(fixture.scopes, fixture.options)
5655

57-
verify(fixture.application, never()).registerActivityLifecycleCallbacks(any())
56+
verify(fixture.application).registerActivityLifecycleCallbacks(any())
5857
}
5958

6059
@Test
@@ -103,4 +102,34 @@ class FeedbackShakeIntegrationTest {
103102
val sut = fixture.getSut()
104103
sut.close()
105104
}
105+
106+
@Test
107+
fun `enabling shake at runtime starts detection on next activity resume`() {
108+
val sut = fixture.getSut(useShakeGesture = false)
109+
sut.register(fixture.scopes, fixture.options)
110+
111+
whenever(fixture.activity.getSystemService(any())).thenReturn(null)
112+
113+
// Shake is disabled, onActivityResumed should not crash
114+
sut.onActivityResumed(fixture.activity)
115+
116+
// Now enable at runtime and resume a new activity
117+
fixture.options.feedbackOptions.isUseShakeGesture = true
118+
sut.onActivityResumed(fixture.activity)
119+
// Should not throw — shake detection attempted (fails gracefully with null SensorManager)
120+
}
121+
122+
@Test
123+
fun `disabling shake at runtime stops detection on next activity resume`() {
124+
val sut = fixture.getSut(useShakeGesture = true)
125+
sut.register(fixture.scopes, fixture.options)
126+
127+
whenever(fixture.activity.getSystemService(any())).thenReturn(null)
128+
sut.onActivityResumed(fixture.activity)
129+
130+
// Disable at runtime and resume
131+
fixture.options.feedbackOptions.isUseShakeGesture = false
132+
sut.onActivityResumed(fixture.activity)
133+
// Should not throw — detection stopped gracefully
134+
}
106135
}

sentry/api/sentry.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2705,7 +2705,9 @@ public final class io/sentry/Sentry {
27052705
public static fun configureScope (Lio/sentry/ScopeCallback;)V
27062706
public static fun configureScope (Lio/sentry/ScopeType;Lio/sentry/ScopeCallback;)V
27072707
public static fun continueTrace (Ljava/lang/String;Ljava/util/List;)Lio/sentry/TransactionContext;
2708+
public static fun disableFeedbackOnShake ()V
27082709
public static fun distribution ()Lio/sentry/IDistributionApi;
2710+
public static fun enableFeedbackOnShake ()V
27092711
public static fun endSession ()V
27102712
public static fun flush (J)V
27112713
public static fun forkedCurrentScope (Ljava/lang/String;)Lio/sentry/IScopes;

sentry/src/main/java/io/sentry/Sentry.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,6 +1371,24 @@ public static void showUserFeedbackDialog(
13711371
options.getFeedbackOptions().getDialogHandler().showDialog(associatedEventId, configurator);
13721372
}
13731373

1374+
/**
1375+
* Enables shake-to-report for user feedback. When enabled, shaking the device will show the
1376+
* feedback dialog. Takes effect on the next activity transition. This can be toggled at runtime.
1377+
*/
1378+
@ApiStatus.Experimental
1379+
public static void enableFeedbackOnShake() {
1380+
getCurrentScopes().getOptions().getFeedbackOptions().setUseShakeGesture(true);
1381+
}
1382+
1383+
/**
1384+
* Disables shake-to-report for user feedback. Takes effect on the next activity transition. If a
1385+
* shake occurs before the transition, the dialog will not be shown.
1386+
*/
1387+
@ApiStatus.Experimental
1388+
public static void disableFeedbackOnShake() {
1389+
getCurrentScopes().getOptions().getFeedbackOptions().setUseShakeGesture(false);
1390+
}
1391+
13741392
/**
13751393
* Sets an attribute on the scope.
13761394
*

0 commit comments

Comments
 (0)