When testing Flutter applications on physical devices, having an in-app logging dashboard can make debugging significantly easier.
Whether you're testing staging builds, investigating API responses, or reviewing application behavior during QA sessions, on-device logs provide valuable visibility without requiring a laptop or Flutter DevTools.
But there is one important problem that developers often overlook:
What happens when your application logs sensitive information?
Consider a typical API response:
{
"userId": 1029,
"email": "developer@example.com",
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "some_refresh_token",
"profile": {
"phone": "+911234567890"
}
}
Displaying this response directly inside an in-app debugging dashboard could expose authentication credentials and personal information to anyone who can access the device or its screen.
In this article, I'll explain how we approached this problem while building an open-source Flutter debugging toolkit called debug_bubble.
1. Why Redacting Logs Is More Complicated Than It Looks
A simple string replacement might work for a basic example, but real application logs contain much more complex data.
For example:
- Nested JSON objects.
- Lists containing multiple objects.
- CamelCase and snake_case API fields.
- Authentication headers and bearer tokens.
- Sensitive values embedded inside strings.
A reliable logging pipeline should sanitize data before it reaches the in-memory storage layer.
If sensitive information enters your log buffer before sanitization, it may remain accessible through searching, viewing, copying, or exporting logs.
We designed the redaction pipeline in debug_bubble to process data during log ingestion rather than waiting until the dashboard renders it.
2. Key-Based Sensitive Data Redaction
The first layer uses configurable sensitive-key matching.
Instead of checking only exact field names, debug_bubble supports normalized key matching.
For example, these variations can be identified consistently:
accessTokenaccess_tokenACCESS_TOKENaccess-token
The built-in sensitive-key rules cover common fields such as passwords, tokens, authorization headers, cookies, secrets, and credentials.
There is also exact token-based matching for fields such as PIN, OTP, CVV, CVC, and session identifiers.
This helps catch sensitive values across different API naming conventions.
3. Regex-Based Value Pattern Redaction
Key-based matching alone isn't sufficient.
Sometimes sensitive information appears inside a normal string rather than a dedicated JSON field.
For example:
DebugLog.info(
'Authorization response received',
json: {
'message': 'Bearer eyJhbGciOiJIUzI1NiIs...',
},
);
The field name message doesn't indicate that its value contains an authentication token.
That's where regex-based value scanning becomes useful.
DebugRedactionConfig supports custom regular expressions for detecting sensitive patterns.
For example:
DebugRedactionConfig(
valuePatterns: [
RegExp(
r'\bBearer\s+[A-Za-z0-9._~+/-]+=*',
caseSensitive: false,
),
],
)
This allows developers to configure additional masking rules for bearer tokens, email addresses, and other application-specific data.
The matching values are replaced with the configured replacement string.
By default:
[REDACTED]
4. Handling False Positives with an Allowlist
There is another interesting problem with sensitive-key matching.
Consider these fields:
{
"tokenCount": 1500,
"cardType": "debit",
"secretary": "John"
}
A broad substring-based rule could incorrectly identify some legitimate fields as sensitive.
To handle this, we introduced allowedKeys in version 0.2.0.
Example:
DebugRedactionConfig(
allowedKeys: {
'tokenCount',
'cardType',
'secretary',
},
)
Allowlisted fields bypass key-based sensitive matching when their normalized keys match exactly.
However, there is an important security detail:
Allowlisting a key does not disable regex-based value redaction.
If an allowlisted field contains a recognizable bearer token or another configured sensitive pattern, its value is still masked.
5. Redaction Before Storage
One of the most important decisions we made was where sanitization should happen.
A basic implementation might follow this approach:
Capture Log → Store Raw Log → Render Dashboard → Hide Sensitive Values
The problem is that raw sensitive information has already entered memory.
Our approach is different:
Capture Log → Redaction Pipeline → Sanitized Log Entry → Bounded Memory Buffer → Dashboard / Search / Export
This ensures that stored log entries contain sanitized data rather than relying exclusively on UI-level masking.
We also implemented bounded payload traversal to prevent excessively large or deeply nested objects from consuming unnecessary resources.
The implementation includes limits for:
- Maximum traversal depth.
- Maximum processed nodes.
- Maximum collection items.
- Maximum string length.
- Circular-reference handling.
These limits help keep diagnostic logging predictable.
6. Using debug_bubble in a Flutter Application
Instead of implementing a custom logging overlay, storage engine, and sanitization pipeline separately, developers can use our package.
Installation
flutter pub add debug_bubble
Initialize the logging engine
import 'package:debug_bubble/debug_bubble.dart';
void main() {
DebugLog.init(
const DebugBubbleConfig(
enabled: true,
maxEntries: 1000,
minLevel: DebugLogLevel.debug,
consoleOutput: true,
captureUncaughtErrors: true,
),
);
runApp(const MyApp());
}
Attach the floating dashboard
MaterialApp(
builder: (context, child) => DebugBubble(
child: child ?? const SizedBox.shrink(),
),
home: const HomeScreen(),
);
The floating bubble provides access to the in-app debugging dashboard without requiring developers to manually manage navigation routes.
7. Configuring Custom Redaction Rules
You can extend the default security rules with application-specific fields.
DebugRedactionConfig supports custom sensitive keys, token matching, allowlisted keys, regex-based patterns, and replacement strings.
This allows developers to combine built-in rules with their own application-specific requirements.
8. What About Production Builds?
Another important consideration when building debugging utilities is their impact on production applications.
We designed debug_bubble to be completely inert in release mode.
When compiled with kReleaseMode:
- Logging calls are dropped immediately.
- No log entries are stored.
- Background error hooks are not installed.
- The floating overlay is not created.
- The widget returns the original child subtree.
This means developers can integrate the package into their application architecture without introducing an active debugging dashboard into release builds.
The package also has zero third-party runtime dependencies, relying on Flutter and Dart standard libraries.
Conclusion
Building an in-app logging dashboard is not just about displaying messages on a screen.
Memory management, payload handling, error interception, and sensitive-data protection are equally important parts of the architecture.
With debug_bubble, we wanted to bring these capabilities together into a reusable Flutter debugging toolkit.
The project is open source, and contributions, feedback, and suggestions are welcome.
Explore the project:
- Pub.dev: https://pub.dev/packages/debug_bubble
- GitHub: https://github.com/Satyam-Gawali/debug_bubble
I'd love to hear how other Flutter developers approach sensitive-data redaction in their debugging pipelines.
Do you prefer key-based sanitization, regex-based pattern detection, or combining both approaches? Let's discuss!
Top comments (0)