This guide shows you how to attach arbitrary JSON metadata (user IDs, feature flags, subscription tiers, session data) to every in-app bug report on Android, iOS, Flutter, and via REST API. By the end, every bug report your users submit will arrive with the business context needed to reproduce and prioritize issues, captured automatically alongside device telemetry. You’ll need a Critic account (30-day free trial, no credit card required) and your app’s product access token to follow along.
Why Device Telemetry Alone Falls Short
Two users report “checkout crashes on submit.” Both are on a Pixel 8, Android 14, 4 GB free RAM, connected via WiFi. The device telemetry is identical. But User A is on your free tier with a coupon code applied. User B is on the enterprise plan paying with a saved credit card. The crash only triggers when a coupon discount is calculated against the free tier’s pricing logic.
Without custom metadata, you’re staring at two identical device snapshots and no lead. With metadata ("subscription_tier": "free", "coupon_applied": true) the pattern jumps out from the first two reports.
This scenario plays out constantly. A Rollbar survey of 950+ developers found that 38% spend up to a quarter of their working hours fixing bugs, and 26% spend up to half. Data from Coralogix puts it more starkly: developers spend roughly 75% of their time debugging, about 1,500 hours per year. The bottleneck is rarely writing the fix. It’s reproducing the problem. And reproduction depends on context.
Every bug report carries three layers of context:
- Device telemetry (battery, memory, OS, network). Answers: what hardware and software environment?
- Console logs (the last 500 logcat entries on Android, stderr/stdout on iOS). Answers: what happened technically?
- Custom metadata (user ID, feature flags, session state, business data). Answers: who is this user and what app state were they in?
Most in-app feedback tools stop at layers one and two. Necessary, but insufficient. Layer three, the metadata you define, is where reproduction actually happens. It captures app-specific state that no automated system can infer.
The only existing tutorial on custom metadata in bug reports comes from Marker.io, and it covers their web-only JavaScript SDK. No mobile-focused guide exists for iOS, Android, or Flutter. This guide fills that gap.
Prerequisites
Before starting, make sure you have:
- A Critic account: sign up at critic.inventiv.io (30-day free trial, no credit card required)
- An organization and app created in the Critic dashboard
- Your app’s product access token: found in your app’s settings in the dashboard
- One of the following: an Android app (Java/Kotlin, minSdk 21+), an iOS app (Swift 5+, iOS 12+), a Flutter app (Dart 2.17+), or any HTTP client for REST API integration
-
The Critic SDK installed for your platform:
-
Android:
implementation 'io.inventiv.critic.android:critic-android:1.0.4'in yourbuild.gradle -
iOS:
pod 'Critic', '~> 0.1.5'in yourPodfile, then runpod install -
Flutter:
inventiv_critic_flutter: ^0.4.0in yourpubspec.yaml -
JavaScript/Web:
inventiv-criticvia npm
-
Android:
- Critic initialized with one line of code; see the Getting Started guide for platform-specific setup
If you haven’t integrated Critic yet, initialization is one line of code. The Android SDK is roughly 1,600 lines of Java with minimal dependencies, so it won’t bloat your build.
Designing Your Metadata Schema
Before writing any code, decide what metadata to attach. The guiding principle: include everything that affects app behavior; exclude everything that identifies a person unnecessarily.
Custom metadata in Critic is arbitrary JSON. You’re free to use any key-value structure your debugging workflow needs. Here are three ready-to-use schemas you can adapt.
E-Commerce App Metadata
{
"user_id": "usr_a3f9b2",
"subscription_tier": "free",
"cart_item_count": 3,
"payment_method": "apple_pay",
"coupon_applied": true,
"coupon_code": "SAVE20",
"locale": "en-US",
"feature_flags": {
"new_checkout_flow": true,
"dynamic_pricing": false
}
}
Every field earns its place. coupon_applied combined with subscription_tier would have resolved the opening scenario from two reports instead of two days. payment_method surfaces bugs specific to Apple Pay or Google Pay integrations. cart_item_count reveals whether the crash only happens with large carts (a pagination or memory issue invisible in device telemetry). feature_flags tells you instantly whether the user was on the new checkout flow or the legacy one.
SaaS / B2B App Metadata
{
"user_id": "usr_7d2e1f",
"org_id": "org_4a8c3b",
"role": "admin",
"team_size": 12,
"subscription_plan": "pro",
"feature_flags": {
"beta_dashboard": true,
"legacy_api": false,
"ai_assist": true
},
"active_view": "reports/monthly",
"data_volume": "10k_records"
}
role and team_size help reproduce permission-related bugs that only affect admins on large teams. data_volume surfaces performance issues that vanish during dev testing with 50 records but explode with 10,000. active_view tells you exactly which screen the user was on when they shook the device.
Multi-Tenant / Agency App Metadata
{
"tenant_id": "client_acme",
"user_id": "usr_9f3a7c",
"tenant_plan": "enterprise",
"white_label": true,
"custom_domain": true,
"api_version": "v3",
"feature_flags": {
"custom_branding": true,
"sso_enabled": true
}
}
For agencies managing multiple client apps, this schema maps directly to Critic’s product-based permissions. Each client’s app gets isolated reports with tenant-specific metadata, so when Client Acme reports a branding bug, you immediately see it’s a white-label configuration issue on their custom domain.
PII Considerations
Exclude: full names, email addresses, passwords, payment card numbers, health data, precise geolocation, or any data subject to GDPR/CCPA that you don’t need for debugging.
Safe pattern: use opaque user IDs like usr_a3f9b2 that your backend can resolve when needed, rather than embedding PII directly in metadata. This keeps your bug reporting pipeline free of regulated data while preserving your ability to look up the user.
This aligns with the GDPR data minimization principle: collect only what you need for the stated purpose. Here, the stated purpose is reproducing and fixing bugs.
Step 1: Attach Static Metadata at Initialization (Android)
Start with metadata that applies to every report from this app install (environment, build configuration, and app variant):
// In your Application class onCreate()
Critic.initialize(this, "YOUR_PRODUCT_ACCESS_TOKEN");
JsonObject metadata = new JsonObject();
metadata.addProperty("app_environment", "production");
metadata.addProperty("build_type", BuildConfig.BUILD_TYPE);
metadata.addProperty("app_variant", BuildConfig.FLAVOR);
Critic.setProductMetadata(metadata);
setProductMetadata accepts a JsonObject, not a fixed schema. Add any key-value pairs your team needs. This static metadata provides a baseline: you’ll always know whether a bug occurred in production or staging, in a debug or release build.
Step 2: Update Metadata Dynamically as App State Changes (Android)
Static metadata is a start, but the real debugging power comes from metadata that reflects the current app state. Update it at every significant state transition:
// After user logs in
public void onUserAuthenticated(User user) {
JsonObject metadata = new JsonObject();
metadata.addProperty("user_id", user.getId());
metadata.addProperty("subscription_tier", user.getTier());
metadata.addProperty("role", user.getRole());
JsonObject flags = new JsonObject();
flags.addProperty("new_checkout_flow", FeatureFlags.isEnabled("new_checkout"));
flags.addProperty("dark_mode", FeatureFlags.isEnabled("dark_mode"));
metadata.add("feature_flags", flags);
Critic.setProductMetadata(metadata);
}
// When user enters checkout
public void onCheckoutStarted(Cart cart) {
JsonObject metadata = new JsonObject();
metadata.addProperty("user_id", currentUser.getId());
metadata.addProperty("cart_item_count", cart.getItemCount());
metadata.addProperty("coupon_applied", cart.hasCoupon());
metadata.addProperty("payment_method", cart.getPaymentMethod());
metadata.addProperty("active_flow", "checkout");
JsonObject flags = new JsonObject();
flags.addProperty("new_checkout_flow", FeatureFlags.isEnabled("new_checkout"));
metadata.add("feature_flags", flags);
Critic.setProductMetadata(metadata);
}
Each call to setProductMetadata replaces the previous metadata entirely. The report captures whatever was set last, so keep it current. Think of metadata updates like snapshots: each state transition overwrites the previous one so the report always reflects where the user was when they shook the device.
A lightweight helper method keeps this manageable:
private void updateCriticMetadata() {
JsonObject metadata = new JsonObject();
if (currentUser != null) {
metadata.addProperty("user_id", currentUser.getId());
metadata.addProperty("subscription_tier", currentUser.getTier());
}
metadata.addProperty("active_screen", getCurrentScreenName());
metadata.add("feature_flags", getActiveFlags());
Critic.setProductMetadata(metadata);
}
Call updateCriticMetadata() from your authentication callbacks, navigation router, and feature flag observer. Every report will reflect current state rather than stale initialization data.
Step 3: iOS Implementation (Swift)
The iOS SDK uses a dictionary assigned to the productMetadata property on the shared Critic instance:
// Initialization
Critic.instance().start("YOUR_PRODUCT_ACCESS_TOKEN")
// Set metadata after user authentication
func onUserAuthenticated(_ user: User) {
Critic.instance().productMetadata = [
"user_id": user.id,
"subscription_tier": user.tier,
"role": user.role,
"feature_flags": [
"new_checkout_flow": FeatureFlags.isEnabled(.newCheckout),
"dark_mode": FeatureFlags.isEnabled(.darkMode)
],
"active_view": "home"
]
}
Update metadata on viewDidAppear for screen-specific context, or register NotificationCenter observers for state changes:
NotificationCenter.default.addObserver(
forName: .userDidLogin,
object: nil,
queue: .main
) { _ in
self.updateCriticMetadata()
}
The same principles from the Android section apply: set metadata as early as possible, update at every significant state change, and keep payloads focused on identifiers and states rather than full data objects.
For programmatic report submission on iOS, use NVCReportCreator (the iOS equivalent of Android’s BugReportCreator) to build reports with custom descriptions, metadata, and file attachments.
Step 4: Flutter Implementation (Dart)
The Flutter SDK (inventiv_critic_flutter) provides report creation and submission:
// Initialization
String key = 'YOUR_PRODUCT_ACCESS_TOKEN';
Critic().initialize(key);
// Create and submit a report with metadata
var report = BugReport.create(
description: 'Checkout crashes on submit',
stepsToReproduce: '1. Add item to cart\n2. Apply coupon\n3. Tap submit',
);
// Add file attachments if needed
report.attachments = <Attachment>[
Attachment(name: 'screenshot.png', path: screenshotPath),
];
await Critic().submitReport(report);
For attaching arbitrary JSON metadata to every report in a Flutter app, use the REST API v2 (covered in Step 5). The REST API gives you full metadata control from any platform, including Flutter, and integrates cleanly with Dart’s http package:
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<void> submitReportWithMetadata({
required String description,
required Map<String, dynamic> metadata,
}) async {
var request = http.MultipartRequest(
'POST',
Uri.parse('https://critic.inventiv.io/api/v2/bug_reports'),
);
request.headers['Authorization'] = 'Bearer YOUR_PRODUCT_ACCESS_TOKEN';
request.fields['description'] = description;
request.fields['metadata'] = jsonEncode(metadata);
await request.send();
}
// Usage
await submitReportWithMetadata(
description: 'Checkout crashes on submit',
metadata: {
'user_id': currentUser.id,
'subscription_tier': currentUser.tier,
'feature_flags': {
'new_checkout_flow': featureFlags['new_checkout'],
'dark_mode': featureFlags['dark_mode'],
},
'locale': PlatformDispatcher.instance.locale.toString(),
'active_route': currentRouteName,
},
);
This approach gives Flutter developers the same arbitrary JSON metadata capability available on Android and iOS, with one implementation that works across both platforms.
Step 5: REST API v2
For platforms without native SDKs (desktop apps, smart TVs, CLI tools, or CI/CD pipelines) the REST API gives you full metadata capability over HTTP:
curl -X POST https://critic.inventiv.io/api/v2/bug_reports \
-H "Authorization: Bearer YOUR_PRODUCT_ACCESS_TOKEN" \
-F "description=Checkout crashes on submit" \
-F 'metadata={"user_id":"usr_a3f9b2","subscription_tier":"free","coupon_applied":true,"feature_flags":{"new_checkout_flow":true}}'
The metadata field accepts a JSON string in the multipart form body. You can attach files alongside metadata: screenshots, log exports, or any file your debugging workflow needs.
Use cases beyond mobile:
- CI/CD pipelines submitting automated test failure reports with build metadata (commit SHA, branch, test suite, environment)
- Custom feedback UIs: a “Report issue with this order” button that pre-populates the order ID, items, and payment method as metadata
- QA automation submitting structured reports programmatically during regression testing
The API exposes all functionality available in the web portal. Anything you can do in the dashboard, you can automate via the API.
Step 6: Submit a Test Report
Before moving to production, submit a test report to verify metadata flows end-to-end.
On Android, use BugReportCreator for programmatic submission:
JsonObject metadata = new JsonObject();
metadata.addProperty("user_id", "usr_test_123");
metadata.addProperty("subscription_tier", "enterprise");
metadata.addProperty("test_scenario", "checkout_with_coupon");
JsonObject flags = new JsonObject();
flags.addProperty("new_checkout_flow", true);
metadata.add("feature_flags", flags);
BugReport report = new BugReportCreator()
.description("Test report: verifying metadata appears in dashboard")
.metadata(metadata)
.create(context);
The create() method returns a BugReport object on success or throws a ReportCreationException with details on what went wrong.
When to use which approach:
| Approach | Best for |
|---|---|
setProductMetadata (Android) / productMetadata (iOS) | Enriching the default shake-to-report flow. Set it once, update on state changes; every user-initiated report includes your metadata automatically. |
BugReportCreator (Android) / NVCReportCreator (iOS) | Programmatic submissions. Custom feedback UIs, automated test reports, or any scenario where you control the entire submission flow. |
| REST API v2 | Cross-platform consistency, CI/CD integration, platforms without native SDKs, or Flutter apps needing metadata support. |
Submit a test report, open the dashboard, and see your custom JSON displayed right alongside the automatic device telemetry: battery, memory, disk, network, OS, and console logs. The full picture, in one place.
Verification: Confirming Metadata in the Critic Dashboard
After submitting your test report:
- Log into critic.inventiv.io
- Navigate to your app, then Bug Reports
- Open the most recent report
- Scroll to the Metadata section; your JSON should appear exactly as submitted
- Verify: all keys present, values correct, nested objects (like
feature_flags) displayed as structured JSON
The dashboard displays the report in a structured layout: the user’s description at the top, device telemetry (battery level, memory stats, disk space, network type, OS version) in a detailed panel, and your custom metadata rendered as formatted JSON below. Together, these give you the hardware context and the business context in a single view.
API verification for automated workflows:
curl -X GET https://critic.inventiv.io/api/v2/bug_reports/{report_id} \
-H "Authorization: Bearer YOUR_PRODUCT_ACCESS_TOKEN"
Check the response JSON for the metadata field. This is useful for building automated tests that verify your metadata pipeline: submit a report with known metadata, then programmatically confirm it arrived intact.
Troubleshooting Common Issues
Metadata Fields Missing from Reports
Cause: setProductMetadata (Android) or productMetadata (iOS) was set after the user submitted the report, or was never set in the current session.
Fix: Set metadata as early as possible: immediately after Critic initialization for static values, and again after user authentication for user-specific data. Metadata must be set before the shake-to-report trigger fires. If reports arrive without metadata, add logging around your metadata calls to confirm they execute before submission.
Malformed JSON Errors
Cause: On Android, building JSON via string concatenation instead of JsonObject. On REST API, improperly escaped quotes in the multipart form.
Fix: Always use the platform’s JSON builder: JsonObject on Android, native dictionaries on iOS, jsonEncode() in Dart, or JSON.stringify() in JavaScript. Test your payload with a JSON validator before sending. For cURL, use single quotes around the metadata value: -F 'metadata={"key":"value"}'.
Metadata Payload Too Large
Cause: Attaching large arrays (full cart contents with image URLs) or deeply nested objects.
Fix: Keep metadata focused on identifiers and states, rather than full data objects. Send "cart_item_count": 3 and "cart_total": 49.99 instead of the entire cart array with product details. If you need granular data, include IDs that your backend can resolve: "order_id": "ord_8f2a1b" instead of the full order object.
Stale Metadata on Reports
Cause: Metadata set once at app launch but never updated when user state changes (login, navigation, feature flag toggles).
Fix: Call your metadata update method at every significant state transition. Create a centralized updateCriticMetadata() helper that your auth system, navigation router, and feature flag manager each invoke. This single function guarantees reports always reflect current state.
Metadata Missing via REST API
Cause: The metadata field must be a valid JSON string in the multipart form, not a raw object or unquoted text.
Fix: Wrap your JSON in quotes and ensure it’s properly escaped. Verify your token is valid first; if authentication fails, the issue is the token, not metadata formatting. Test with a minimal payload before adding complexity.
Correlating Bugs with Feature Flags
Include active feature flags in every report’s metadata:
{
"feature_flags": {
"new_checkout_flow": true,
"dynamic_pricing": false,
"beta_search": true
}
}
When 8 out of 10 bug reports about checkout show "new_checkout_flow": true, the correlation is visible without AI triage, without a dedicated analytics pipeline, and without an enterprise contract. You open the dashboard, scan the metadata across recent reports, and spot the flag causing problems.
Enterprise tools take a different approach. Datadog’s Feature Flag Tracking requires integrating their RUM SDK with a supported flag service (LaunchDarkly, Split, Statsig), configuring per-service tracking, and paying for their RUM product. That works for teams with enterprise budgets. Critic achieves the same bug-to-flag correlation through arbitrary JSON metadata that you’re already attaching.
You’re trading a dedicated “Feature Flag Tracking” UI for flexibility. Attach the data, and the dashboard becomes your correlation tool.
For teams that roll out features behind flags (which is most teams shipping frequently) this single metadata pattern can cut the time between “something broke” and “this flag broke it” from days to minutes.
Next Steps
-
Add file attachments alongside metadata. Screenshots, log exports, or custom files that complement the JSON context. Both
BugReportCreatoron Android and the REST API support multiple file attachments per report. - Build a custom feedback UI using the REST API v2. A “Report issue with this order” button that pre-populates order metadata gives users a targeted way to report without the generic shake prompt.
- Invite your team to the Critic dashboard. Comments and email notifications turn metadata-rich reports into a collaborative triage workflow.
- Expand to additional apps. Critic’s per-app pricing ($20/month each) and product-based permissions make it straightforward to add apps or isolate client projects, particularly valuable for agencies managing multiple codebases.
Your bug reports now carry three layers of context: device telemetry, console logs, and your custom business metadata. The next bug your users report will arrive ready to reproduce.