api_sentinel 2.0.0
api_sentinel: ^2.0.0 copied to clipboard
A lightweight Dio-powered API service for Flutter that automatically logs requests
π°οΈ API Sentinel #
A structured, developer-friendly networking and debugging layer for Flutter.
api_sentinel provides a clean and consistent way to handle API requests, error mapping, and runtime debugging through an on-screen overlay.
β¨ Features #
β
Unified API call interface via ApiService.instance.request()
β
Customizable callbacks for success, Dio exceptions, and general exceptions
β
Centralized error handling and message parsing
β
Floating on-screen debug overlay to visualize requests
β
Generate curl commands for every request
β
Copy generated curl with a single tap
β
View request headers
β
View response headers
β
Built with dio and get (GetX) for lightweight reactivity
β
Supports Android, iOS, and Web
β
Built-in Unauthorized detection with callback (unauthorized status code, customizable unauthorized callback)
β
Optional network monitoring callback with structured error params
β
Secret knock gesture to reveal a hidden debug entry point
β
TOTP-gated access to the debug overlay in release builds
π¦ Installation #
Add this line to your pubspec.yaml:
dependencies:
api_sentinel: ^2.0.0
Then run:
flutter pub get
π§ Architecture Overview #
The library is built around three core layers:
| Layer | Description |
|---|---|
| ApiService | Handles all HTTP requests (GET, POST, PUT, PATCH, DELETE) through Dio. |
| ErrorHandler | Converts all errors (network, timeout, response, etc.) into readable Failure objects. |
| DebugOverlay | Shows all ongoing and past requests on top of your UI during runtime (toggleable). |
| SecretKncock and Totp Flow | Trigger via custom secret knock and accept Authenticator secret 6 digit codes. |
βοΈ Usage #
1οΈβ£ Initialize the Service #
ApiService.instance.init(
baseUrl: 'YOUR_BASE_URL',
needToShowLog: false,
needToLogRequests: false,
unauthorizedStatusCode: 401,
onUnauthorizedCallBack: () {
// Handle session expiration or redirect to login.
},
networkMonitoringFunction: (params) {
print('Request URL: ${params.requestUrl}');
print('Status Code: ${params.statusCode}');
print('API Error: ${params.apiErrorMessage}');
print('Runtime Error: ${params.runTimeErrorType}');
}
);
2οΈβ£ Monitor Network Failures #
networkMonitoringFunction receives a NetworkMonitoringParams object whenever a request fails with either a DioException or another runtime exception.
class NetworkMonitoringParams {
final StackTrace? stackTrace;
final String? requestUrl;
final int? statusCode;
final String? apiErrorMessage;
final String? errorMessage;
final Object? runTimeErrorType;
}
Use it to capture the request URL, HTTP status code, parsed API error message, raw error text, and the original runtime error object in one place.
3οΈβ£ Make a Request #
Each request is wrapped with ApiService.instance.request()
This ensures that error handling, logging, and overlay integration all happen automatically.
await ApiService.instance.request(
method: HttpMethod.get,
url: 'SOME_REQUEST',
onSuccess: (response) {
print('β
Success: ${response.data}');
},
onCatchDioException: (error) {
print('β Dio Error: ${handleErrorMessage(error)}');
},
onCatchException: (error) {
print('π₯ Exception: ${handleErrorMessage(error)}');
},
);
This pattern applies to any HTTP method β just change the method and url.
4οΈβ£ Supported HTTP Methods #
You can use all standard HTTP verbs through the HttpMethod enum:
enum HttpMethod { get, post, put, patch, delete }
π§© Error Architecture #
| Error Type | Source | Failure Example |
|---|---|---|
| Connection Timeout | Dio | (-1) Connection timed out |
| Bad Request | HTTP 400 | Bad Request |
| Unauthorized | HTTP 401 | Unauthorized access |
| Forbidden | HTTP 403 | Access denied |
| Not Found | HTTP 404 | Resource not found |
| Server Error | HTTP 500 | Internal server error |
| Cancelled | Dio CancelToken | Request cancelled |
| Unknown | Fallback | Something went wrong |
π Secret Knock & TOTP-Gated Debug Access (v2.0.0) #
In release builds, the debug overlay should stay hidden until an authorized user proves access. API Sentinel provides a secret knock gesture detector, a TOTP verification flow, and an AccessController that unlocks the log overlay after a valid code.
Flow overview #
User performs knock pattern on a widget
β
SecretKnockDetector fires onSecretKnock
β
You show TotoSecretSection (dialog, bottom sheet, etc.)
β
User enters 6-digit TOTP from authenticator app
β
AccessController.validateCode() succeeds
β
AccessController.enableDebugFeatures() β isDebugFeaturesAccessible = true
β
DebugOverlayWidget becomes visible
1οΈβ£ Register AccessController #
Register the controller once near the root of your app (before any widget that needs it):
import 'package:api_sentinel/api_sentinel.dart';
import 'package:get/get.dart';
class _MyAppState extends State<MyApp> {
final AccessController accessController = Get.put(
AccessController(),
tag: AllControllerKeys.accessControllerKey,
);
// ...
}
| Symbol | Role |
|---|---|
AccessController |
Holds TOTP secret, validates codes, exposes isDebugFeaturesAccessible |
AllControllerKeys.accessControllerKey |
Stable GetX tag so TotoSecretSection can Get.find the same instance |
Key APIs
| Method / property | Description |
|---|---|
initialize() |
Loads or generates the TOTP secret. Call early (e.g. when the OTP UI opens). |
validateCode(String code) |
Returns true when the 6-digit code matches (Β±1 time window). |
enableDebugFeatures() |
Sets isDebugFeaturesAccessible to true. |
isDebugFeaturesAccessible |
Reactive RxBool β gate the overlay behind this. |
resetSecret() |
Clears the dev-only secure-storage secret (debug builds without dart-define). |
2οΈβ£ Wrap a widget with SecretKnockDetector #
Pick one knock pattern for your app and use it consistently. The example uses SecretPatterns.accessible (double-tap, then long-press):
SecretKnockDetector(
knockPattern: SecretPatterns.accessible,
onSecretKnock: () {
showDialog(
context: context,
builder: (_) => Dialog(
child: const TotoSecretSection(),
),
);
},
child: const Text('App title'), // any widget β title, logo, version label, etc.
)
Available patterns (SecretPatterns):
| Pattern | Gesture |
|---|---|
accessible |
Double-tap β long-press |
tripleLongPress |
Long-press Γ 3 |
shaveAndHaircut |
Tap β tap β long-press |
morseSOS |
TapΓ3 β long-pressΓ3 β tapΓ3 |
You can also pass a custom List<KnockType> to knockPattern. Steps must be completed within resetTimeout (default 2 seconds between steps).
3οΈβ£ Show TotoSecretSection on knock #
TotoSecretSection is a pre-built OTP field. It finds AccessController by tag, calls initialize() on mount, validates on submit, and calls enableDebugFeatures() on success.
Use it wherever you like β dialog, bottom sheet, or full-screen route:
onSecretKnock: () {
showModalBottomSheet(
context: context,
builder: (_) => const Padding(
padding: EdgeInsets.all(24),
child: TotoSecretSection(),
),
);
},
You can also build your own UI and call accessController.validateCode(code) directly.
4οΈβ£ Gate DebugOverlayWidget behind access #
Only show the floating log button after TOTP succeeds:
Stack(
children: [
const MyHomePage(),
Obx(() {
if (!accessController.isDebugFeaturesAccessible.value) {
return const SizedBox.shrink();
}
return const DebugOverlayWidget();
}),
],
)
In debug builds without dart-defines, the first run logs an otpauth:// URI to the console β scan it with Google Authenticator. In release builds, the secret comes from dart-define (see below).
5οΈβ£ Pass the TOTP secret with --dart-define #
For release (and local testing that matches release), split your Base32 secret into two parts so neither half is useful alone:
flutter build apk --release \
--dart-define=TOTP_SECRET_PART1="FIRST_HALF_OF_BASE32_SECRET" \
--dart-define=TOTP_SECRET_PART2="SECOND_HALF_OF_BASE32_SECRET"
The app concatenates both parts at compile time: TOTP_SECRET_PART1 + TOTP_SECRET_PART2.
Local run with embedded secret:
flutter run --release \
--dart-define=TOTP_SECRET_PART1="JBSWY3DPEHPK3PXP" \
--dart-define=TOTP_SECRET_PART2="GEZDGNBVGY3TQOJQ"
Important: Register the full combined secret in your authenticator app, not each part separately.
CI / Xcode / Gradle
Add the same defines to your build pipeline:
# GitHub Actions example
- run: flutter build ipa --release
--dart-define=TOTP_SECRET_PART1=${{ secrets.TOTP_PART1 }}
--dart-define=TOTP_SECRET_PART2=${{ secrets.TOTP_PART2 }}
| Build mode | Secret source |
|---|---|
| Release | TOTP_SECRET_PART1 + TOTP_SECRET_PART2 (required) |
| Debug + dart-define | Same embedded secret (for testing release behaviour) |
| Debug, no dart-define | Auto-generated secret stored in secure storage; URI printed to console |
Full integration example #
See example/lib/main.dart for a working setup with SecretKnockDetector, TotoSecretSection, AccessController, and DebugOverlayWidget.
π§° Debug Overlay #
π§ Floating Draggable Widget #
A persistent, draggable button gives quick access to real-time logs. In production, gate it behind AccessController.isDebugFeaturesAccessible (see Secret Knock & TOTP).
Stack(
children: [
const MyHomePage(),
Obx(() {
if (!accessController.isDebugFeaturesAccessible.value) {
return const SizedBox.shrink();
}
return const DebugOverlayWidget();
}),
],
)
Inside, youβll see:
- A real-time log list for each request
- Search and filter by method/status code
- Tap any log to expand request/response JSON
- Toggle between Tree View and Pretty JSON
- Click to expand full-screen
- View request headers
- View response headers
- Generate a curl command from the request
- Copy the generated curl to the clipboard
π³ JSON Tree Viewer #
Displays structured hierarchical JSON for nested inspection.
π¨ Pretty JSON Viewer #
Shows syntax-colored formatted JSON text.
π₯ Full-Screen View #
Click the expand icon (π) in the corner to open the full JSON view for better readability.
Whenever your app performs an API call through ApiService, it will appear in a floating overlay with:
- Method type (GET/POST/PUT/PATCH/DELETE)
- Status code
- Response time
- Response preview
- Request headers
- Response headers
- Generated curl command with one-tap copy
π§ͺ Example Project #
A complete example app is included under the example/ directory.
Debug mode (auto-generated TOTP secret printed to console):
cd example
flutter run
With embedded TOTP secret (same as release):
cd example
flutter run \
--dart-define=TOTP_SECRET_PART1="YOUR_PART_1" \
--dart-define=TOTP_SECRET_PART2="YOUR_PART_2"
The example demonstrates:
- API calls via
ApiService(GET, POST, PUT, PATCH, DELETE) SecretKnockDetectorwithSecretPatterns.accessibleTotoSecretSectionshown in a dialog after the knockDebugOverlayWidgetgated behindAccessController.isDebugFeaturesAccessible
π License #
MIT License Β© 2025 Developed and maintained by Aref Yazdkhasti
π¬ Contribution #
Contributions are welcome!
If youβd like to improve the debugging UI, extend the ErrorHandler, or support additional APIs, open a PR or issue.
π§ Future Plans #
- β Response caching layer
- β Retry strategy for failed requests and 401 unauthenticated request
- β Filterable API session logs
API Sentinel β Because understanding your API should be as clear as your code.