api_sentinel 2.0.2
api_sentinel: ^2.0.2 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.2
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: NetworkMonitoringFunction(
function: (params) {
print('Request URL: ${params.requestUrl}');
print('Status Code: ${params.statusCode}');
print('API Error: ${params.apiErrorMessage}');
print('Runtime Error: ${params.runTimeErrorType}');
},
),
exceptionMonitoringFunctions: ExceptionMonitoringFunction(
function: (params) {
// Send exception information to your monitoring service.
// Example: Sentry, Crashlytics, etc.
print('Exception: ${params.exception}');
print('StackTrace: ${params.stackTrace}');
print('Request URL: ${params.requestUrl}');
},
),
);
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οΈβ£ Monitor Runtime Exceptions #
exceptionMonitoringFunctions provides an optional callback for monitoring unexpected runtime exceptions that occur while processing an API request.
Unlike networkMonitoringFunction, which receives structured information about network and API failures, this callback is triggered when an exception is caught outside the DioException flow. It receives an ExceptionMonitoringParams object with the caught exception, stack trace, and request URL.
class ExceptionMonitoringParams {
final Object exception;
final StackTrace stackTrace;
final String? requestUrl;
final String? errorMessage;
}
This is useful for reporting errors such as:
- Response parsing errors
- Type casting errors
- Unexpected runtime exceptions
- Model conversion failures
- Other non-Dio exceptions that occur during request handling
4οΈβ£ 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.
5οΈβ£ 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.