zap_bugs
A Flutter package that detects device shakes, captures a screenshot, shows a feedback dialog, and submits the report — with a built-in GitHub Issues integration.
Features
- Shake detection (powered by
shake) with tuneable sensitivity - In-app screenshot capture at shake time
- Material dialog — fully themed via
DialogTheme,InputDecorationTheme,TextButtonThemeData, andFilledButtonThemeData - Fully injectable strings — supply your own copy or translations via
ZapBugsStrings - Optional reporter name field that is persisted locally per device for future reports
FeedbackServiceabstract class — implement once to send feedback anywhere (Jira, Linear, Slack, your own API…)- Built-in
GitHubFeedbackService— creates a GitHub issue with device info and an uploaded screenshot - Web-safe: shake detection is silently skipped on Flutter Web
- Beta-friendly: works in TestFlight and Google Play internal testing builds since you control when to enable it
Installation
Add to your pubspec.yaml:
dependencies:
zap_bugs: ^0.1.0
Then run:
flutter pub get
GitHub Issues integration
Step 1 — Create a Personal Access Token (PAT)
- Go to GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens.
- Under Repository access, select Only select repositories and choose the single repo your app reports to. Do not grant access to any other repository.
- Grant only the minimum permissions needed on that repository:
- Contents — Read and write (required to upload screenshots as committed files)
- Issues — Read and write (required to create issues)
- Copy the token — you will not be able to see it again.
Security: Scope the token to a single repository, never to all repositories.
Never hard-code the token in source code or commit it to version control.
Pass it at build time using--dart-defineor a secrets manager.
Step 2 — Pass both defines at build time
The token and the enabled flag travel together:
# Debug / local
flutter run \
--dart-define=SHAKE_FEEDBACK_ENABLED=true \
--dart-define=GITHUB_FEEDBACK_TOKEN=ghp_xxxxxxxxxxxx
# TestFlight / Google Play internal testing (release archive, feature still on)
flutter build ipa \
--dart-define=SHAKE_FEEDBACK_ENABLED=true \
--dart-define=GITHUB_FEEDBACK_TOKEN=$GITHUB_FEEDBACK_TOKEN
# Production (feature off — token can be omitted entirely)
flutter build ipa
For CI/CD, store both as secrets and inject them the same way.
Step 3 — Wire up the package
Note:
ZapBugsController.initis a no-op on web but active on all other platforms, including release builds. This is intentional — TestFlight and Google Play internal testing use release archives and still need shake feedback. Gate the call with your own flag so production builds stay clean.
main.dart
import 'package:flutter/material.dart';
import 'package:zap_bugs/zap_bugs.dart';
const _shakeFeedbackEnabled =
bool.fromEnvironment('SHAKE_FEEDBACK_ENABLED', defaultValue: false);
final _navigatorKey = GlobalKey<NavigatorState>();
void main() {
runApp(
RepaintBoundary(
key: ZapBugsController.screenshotKey, // required for screenshots
child: MyApp(navigatorKey: _navigatorKey),
),
);
if (_shakeFeedbackEnabled) {
ZapBugsController.init(
contextProvider: () => _navigatorKey.currentContext,
service: GitHubFeedbackService(
GitHubFeedbackConfig(
token: const String.fromEnvironment('GITHUB_FEEDBACK_TOKEN'),
owner: 'my-org', // GitHub user or organisation
repo: 'my-app', // repository name
),
),
);
}
}
Step 4 — Dispose when the app exits
AppLifecycleListener(
onExitRequested: () async {
ZapBugsController.dispose();
return AppExitResponse.exit;
},
);
That's it — shake the device and a GitHub issue will be created automatically.
GitHubFeedbackConfig reference
| Parameter | Type | Default | Description |
|---|---|---|---|
token |
String |
required | GitHub PAT with Contents + Issues write access |
owner |
String |
required | Repository owner (user or org) |
repo |
String |
required | Repository name |
screenshotsPath |
String |
'feedback-screenshots' |
Path inside the repo where screenshots are committed |
labels |
List<String> |
['feedback'] |
Labels applied to every created issue |
ZapBugsController.init reference
| Parameter | Type | Default | Description |
|---|---|---|---|
contextProvider |
BuildContext? Function() |
required | Returns the current context — typically navigatorKey.currentContext |
onSubmit |
OnFeedbackSubmit? |
null |
Raw submit callback; takes precedence over service |
service |
FeedbackService? |
null |
Any FeedbackService implementation |
strings |
ZapBugsStrings? |
null |
Static copy override; falls back to built-in defaults when omitted |
stringsBuilder |
ZapBugsStrings Function(BuildContext)? |
null |
Lazy, context-aware strings (best for AppLocalizations); takes precedence over strings |
minimumShakeCount |
int |
1 |
Number of shakes required to trigger |
shakeSlopTimeMS |
int |
500 |
Minimum ms between shakes |
shakeCountResetTime |
int |
3000 |
ms after which the shake count resets |
shakeThresholdGravity |
double |
2.7 |
Sensitivity — lower values trigger more easily |
Localization patterns
Use either approach depending on your app architecture:
- Use
stringswhen you have static copy (or non-context translation systems). - Use
stringsBuilderwhen your app uses context-based localization such asAppLocalizations.
ZapBugsController.init(
contextProvider: () => _navigatorKey.currentContext,
service: GitHubFeedbackService(config),
stringsBuilder: (context) {
final l10n = AppLocalizations.of(context)!;
return ZapBugsStrings(
dialogTitle: l10n.feedbackDialogTitle,
descriptionLabel: l10n.feedbackDialogDescription,
submitButton: l10n.feedbackSubmitButton,
cancelButton: l10n.cancel,
successMessage: l10n.feedbackSubmitSuccess,
errorMessage: l10n.feedbackSubmitError,
);
},
);
Resolution precedence is:
stringsBuilder(context)strings- built-in
ZapBugsStrings()defaults
Custom backend
Implement FeedbackService to send feedback anywhere:
class LinearFeedbackService extends FeedbackService {
@override
Future<void> submit(
FeedbackSubmission submission,
Uint8List? screenshotBytes,
) async {
// `submission.reporter` contains the optional reporter name.
// POST to Linear, Jira, Slack webhook, etc.
}
}
ZapBugsController.init(
contextProvider: () => _navigatorKey.currentContext,
service: LinearFeedbackService(),
);
Customising the dialog
Override these ThemeData properties in your app theme:
ThemeData(
dialogTheme: DialogTheme(
backgroundColor: Colors.grey[900],
titleTextStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(foregroundColor: Colors.white70),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(backgroundColor: Colors.deepPurple),
),
)
You can customize reporter field copy through ZapBugsStrings:
const ZapBugsStrings(
reporterLabel: 'Reporter',
reporterHint: 'Your name',
)
Troubleshooting
Shake is not detected
- Ensure
ZapBugsController.init(...)is called. - Confirm the feature flag is enabled (
SHAKE_FEEDBACK_ENABLED=true) in test/beta builds. - Try lowering
shakeThresholdGravityif trigger sensitivity is too strict.
Dialog never appears
- Make sure
contextProviderreturns a valid, mounted context (commonlynavigatorKey.currentContext). - Verify your app has a
MaterialApp/ScaffoldMessengerin the widget tree.
Screenshot is missing
- Ensure your app root is wrapped in a
RepaintBoundarywithkey: ZapBugsController.screenshotKey. - Screenshot capture failures are handled gracefully; feedback can still be submitted without an image.
GitHub submission fails
- Validate PAT permissions (
Contents: Read and write,Issues: Read and write). - Ensure token, owner, and repo values are correct and token is not expired/revoked.
License
MIT
Libraries
- zap_bugs
- A Flutter package that detects device shakes, captures a screenshot, shows a feedback dialog, and submits the result via a configurable callback.