Flutter

Docs

Flutter SDK

Install the Rejourney Flutter SDK for native iOS and Android session replay, privacy masking, crash and ANR reporting, network timing, and product analytics.

Use AI to integrate faster

Using an AI coding assistant? Give it this page and ask it to add the SDK only after your app's consent decision, wire the navigator observer, and mask every field containing credentials, payment data, health data, or other sensitive content.

Flutter SDK

The Rejourney Flutter package provides privacy-first session replay, native crash and ANR reporting, product events, network timing, and SDK health metrics for iOS and Android. Capture runs in native code while the public API, navigation integration, privacy masks, error hooks, and HTTP client are idiomatic Dart and Flutter.

Rejourney Replay Workbench showing a captured iOS session with synchronized console and network evidence

Requirements

PlatformMinimum
Flutter3.22
Dart3.3
iOS15.1
AndroidAPI 24

Installation

Add the package:

flutter pub add rejourney

For a local checkout of this repository, use a path dependency instead:

dependencies:
rejourney:
path: ../../packages/rejourney

Run CocoaPods after adding the plugin to an existing iOS application if your normal Flutter build does not do so automatically:

cd ios && pod install && cd ..

3 Line Setup

Configure once during startup and begin recording only after any consent your product requires:

import 'package:rejourney/rejourney.dart';
await Rejourney.init('pk_live_your_public_key');
await Rejourney.start();

Rejourney.init never starts capture. This separation lets the application wait for an explicit consent result. Call Rejourney.stop() if consent is revoked.

if (await consentStore.canRecord()) {
final result = await Rejourney.start();
debugPrint('session=${result.sessionId} replay=${!result.telemetryOnly}');
}

Remote Recording Settings

Each start reads the project's current recording configuration. When the service is temporarily unavailable, the native SDK uses its cached configuration when possible and otherwise falls back to the local safe defaults.

SettingBehavior
Rejourney enabledRemote kill switch for all session collection.
Recording enabledEnables visual replay. Telemetry-only sessions may continue when replay is off.
Sample rateSampled-out sessions do not start native capture or upload work.
Maximum durationRolls over or ends sessions that reach the configured limit.
Recording FPSControls permitted replay capture frequency.
Text input privacyMasks all text fields or secure fields only, based on project policy.
Image/video privacyMasks image and video regions in native capture.

Remote policy can make capture more restrictive than application configuration; it does not grant consent on the application's behalf.

Screen Tracking

Create one observer and add it to the root application navigator:

final rejourneyObserver = RejourneyNavigatorObserver();
MaterialApp(
navigatorObservers: <NavigatorObserver>[rejourneyObserver],
routes: <String, WidgetBuilder>{
'/': (_) => const HomeScreen(),
'/checkout': (_) => const CheckoutScreen(),
},
);

The observer handles push, pop, replace, and remove operations and suppresses duplicate screen names. You can customize names:

final observer = RejourneyNavigatorObserver(
routeNameResolver: (route) => route.settings.name?.replaceFirst('/', ''),
);

With a declarative Router package, call Rejourney.trackScreen('checkout') from its route-change callback or attach the observer through that package's navigator integration.

User Identification

Associate an internal identifier with the current and future session:

await Rejourney.setUserIdentity('user_abc123');
// Clear it during logout.
await Rejourney.clearUserIdentity();

Use an internal ID or UUID. Hash email addresses or phone numbers before sending them if your product must use those values.

Custom Events

Track meaningful user actions to understand behavior patterns, debug issues, and filter session replays in the dashboard.

Basic Usage

// Simple event (name only)
await Rejourney.logEvent('checkout_started');
// Event with properties
await Rejourney.logEvent('purchase_completed', <String, Object?>{
'transactionId': order.id,
'amount': order.total,
'currency': 'USD',
'paymentProvider': 'stripe',
});

API

Future<void> Rejourney.logEvent(
String name, [
Map<String, Object?> properties = const <String, Object?>{},
])
ParameterTypeRequiredDescription
nameStringYesEvent name — use snake_case for consistency
propertiesMap<String, Object?>NoKey-value pairs attached to this specific event occurrence

Naming Conventions

Use stable, lowercase snake_case names. The preferred names below give Rejourney the cleanest signal for revenue, lifecycle, and future analysis features. The aliases are accepted for compatibility, but new instrumentation should use the preferred name.

User actionPreferred event nameCompatible aliases
Purchase completedpurchase_completedpurchase_complete, purchase_success, purchase, order_completed, complete_purchase, conversion
Checkout startedcheckout_startedcheckout_start, begin_checkout
Product added to cartadd_to_cartcart_add, addtocart, added_to_cart, product_added_to_cart
Product viewedproduct_viewview_item, viewproduct
Signup completedsignup_completedsignup, sign_up, register, account_created
Loginloginsign_in
Paywall viewedpaywall_viewpaywall_exposure, view_paywall
Plan selectedplan_selectedpricing_plan_selected, select_plan, pricing_viewed
Coupon usedcoupon_usecoupon_used, apply_coupon
Trial startedtrial_startedtrial_start, begin_trial
Subscription startedsubscription_startedsubscription_start
Refund processedrefund_processedrefund, refund_completed, refunded
Subscription cancelledsubscription_cancelledcancel, cancel_subscription, cancellation
Payment failedpayment_failedpayment_failure, charge_failed
Onboarding completedonboarding_completedonboarding_milestone, complete_onboarding
Feature usedfeature_usedkey_feature_used, use_feature
Ad viewedad_viewedad_impression, ad_seen, ad_shown, ad_displayed
Ad clickedad_clickedad_click, ad_tapped, ad_cta_clicked
Native ad viewednative_ad_viewednative_ad_impression, native_ad_shown
Native ad clickednative_ad_clickednative_ad_click, native_ad_tapped
Banner ad viewedbanner_ad_viewedbanner_ad_impression, banner_ad_shown
Banner ad clickedbanner_ad_clickedbanner_ad_click, banner_ad_tapped
Interstitial ad viewedinterstitial_ad_viewedinterstitial_ad_impression, interstitial_ad_shown
Interstitial ad clickedinterstitial_ad_clickedinterstitial_ad_click, interstitial_ad_tapped
Rewarded ad viewedrewarded_ad_viewedrewarded_ad_impression, rewarded_video_ad_viewed
Rewarded ad clickedrewarded_ad_clickedrewarded_ad_click, rewarded_video_ad_clicked
Video ad viewedvideo_ad_viewedvideo_ad_impression, video_ad_started
Video ad clickedvideo_ad_clickedvideo_ad_click, video_ad_tapped
Sponsored ad viewedsponsored_ad_viewedsponsored_ad_impression, sponsored_content_viewed
Sponsored ad clickedsponsored_ad_clickedsponsored_ad_click, sponsored_content_clicked
Variant selectedvariant_selectedproduct_variant_selected, select_variant
Size selectedsize_selectedproduct_size_selected, select_size
Color selectedcolor_selectedproduct_color_selected, select_color
Quantity changedquantity_changedquantity_updated, cart_quantity_changed
Reviews viewedreviews_viewedproduct_reviews_viewed, ratings_viewed
Shipping info viewedshipping_info_viewedshipping_details_viewed, delivery_info_viewed
Return policy viewedreturn_policy_viewedreturns_viewed, refund_policy_viewed
Wishlist addwishlist_addadd_to_wishlist, added_to_wishlist
Share clickedshare_clickedshare_tapped, product_shared
Upgrade clickedupgrade_clickedupgrade_cta_clicked, click_upgrade
Upgrade completedupgrade_completedupgraded, plan_upgraded
Discount offer vieweddiscount_offer_vieweddiscount_viewed, promo_offer_viewed
Discount offer accepteddiscount_offer_accepteddiscount_accepted, offer_accepted
Cancel flow startedcancel_flow_startedcancellation_started, subscription_cancel_flow_started
Cancel confirmedcancel_confirmedcancellation_confirmed, subscription_cancel_confirmed

Use these property names when they apply:

MeaningPreferred propertyCompatible aliases
Transaction idtransactionIdtransaction_id
Order idorderId
Money amountamountvalue, price, cartValue, cart_value
Currencycurrency
Quantityquantityqty
Product idproductIdproduct_id, sku
Plan idplanIdplan_id, plan
Price idpriceIdprice_id
Payment providerpaymentProviderpayment_provider
Renewal flagisRenewalis_renewal
Trial conversion flagisTrialConversionis_trial_conversion
Coupon codecouponCodecoupon_code, coupon

Avoid inventing special Rejourney-only object fields. If your product already has useful domain properties, send them as ordinary custom event properties, but do not expect the SDK to infer app-specific business objects.

Examples

// E-commerce
await Rejourney.logEvent('purchase_completed', <String, Object?>{
'transactionId': order.id,
'planId': 'pro',
'amount': 29.99,
'currency': 'USD',
'paymentProvider': 'stripe',
'isRenewal': false,
});
// Onboarding
await Rejourney.logEvent('onboarding_step', <String, Object?>{
'step': 3,
'stepName': 'profile_setup',
'skipped': false,
});
// Feature usage
await Rejourney.logEvent('feature_used', <String, Object?>{
'feature': 'dark_mode',
'enabled': true,
});
// Errors / edge cases
await Rejourney.logEvent('payment_failed', <String, Object?>{
'errorCode': 'card_declined',
'retryCount': 2,
});

Revenue Mapping

The Revenue impact Custom events source uses the same logEvent payload. Use a real money-collected event such as purchase_completed; do not map setup or device events such as device_info, app_initialized, or screen-view events as revenue.

For the e-commerce example above, choose:

Dashboard fieldValue
Purchase eventpurchase_completed
Amount propertyamount
Currency propertycurrency
Default currencyUSD
Amount unitDollars / major units

transactionId is strongly recommended so retries and duplicate client/backend sends collapse into one revenue fact. If your event sends cents, for example <String, Object?>{'amount': 2999, 'currency': 'USD'}, choose Cents / minor units. Refund and lifecycle events are optional; leave them unset unless you log separate events such as refund_completed or subscription_cancelled.

How Events Appear in the Dashboard

Custom events are stored per-session and visible in two places:

  1. Session Replay Timeline — Events appear as markers on the replay timeline so you can jump to the exact moment an action occurred.
  2. Session Archive Filters — Filter the session list by:
    • Event name — Find all sessions containing a specific event (e.g. purchase_completed)
    • Event property — Narrow further by property key and/or value (e.g. planId = pro)
    • Event count — Find sessions with a specific number of custom events (e.g. more than 5 events)

Best Practices

[!TIP]

  • Use consistent naming (snake_case, e.g. button_tapped not Button Tapped)
  • Keep property values simple (strings, numbers, booleans) — avoid deeply nested objects
  • Focus on actions that matter for debugging or analytics — don't log everything
  • Properties are for per-event context. For session-level attributes, use Metadata instead

Metadata

Metadata describes the session and replaces the current value for a key:

await Rejourney.setMetadata('plan', 'premium');
await Rejourney.setMetadata(<String, Object?>{
'role': 'admin',
'checkoutVariant': 'v2',
});

Channel values may be strings, numbers, booleans, null, lists, or maps with string keys. Unsupported objects are converted to strings.

Privacy Controls

RejourneyMask hides a Flutter-rendered region in captured replay frames without changing what the user sees:

RejourneyMask(
child: TextFormField(
obscureText: true,
autofillHints: const <String>[AutofillHints.creditCardNumber],
),
)

The mask tracks layout changes, scrolling, and disposal. It complements native secure-field detection and project-level text/media privacy rules. Mask the smallest useful subtree and test the exact checkout, login, health, and account flows your application ships.

Error Capture

Install error handlers before runApp:

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Rejourney.init('pk_live_your_public_key');
final capture = RejourneyErrorCapture.install();
RejourneyErrorCapture.runGuarded(() {
runApp(const App());
});
}

This records Flutter framework exceptions, platform-dispatcher errors, and guarded-zone errors, then preserves previously installed handlers. Native crash and Android ANR capture are separately controlled by captureCrashes and captureAnrs.

Rejourney.debugCrash() intentionally crashes the native process and Rejourney.debugTriggerAnr() intentionally blocks the native main thread. Use them only in a disposable debug build.

Network Capture

Wrap package:http calls when you want request timeline markers:

final client = RejourneyHttpClient();
try {
final response = await client.get(Uri.parse('https://api.example.com/catalog'));
// Consume response.body as usual.
} finally {
client.close();
}

The wrapper records method, URL, status, timing, content type, and byte sizes. It does not inspect bodies. Rejourney ingestion endpoints are excluded automatically; configure additional patterns with networkIgnoreUrls.

For Dio, gRPC, GraphQL, or another client, use its interceptor to call Rejourney.logNetworkRequest(RejourneyNetworkRequest(...)) after completion.

Configuration Reference

await Rejourney.init(
'pk_live_your_public_key',
config: const RejourneyConfig(
enabled: true,
observeOnly: false,
captureQuality: RejourneyCaptureQuality.medium,
stopTimeout: Duration(seconds: 10),
detectRageTaps: true,
rageTapThreshold: 3,
rageTapTimeWindow: Duration(milliseconds: 500),
captureCrashes: true,
captureAnrs: true,
autoTrackNetwork: true,
networkIgnoreUrls: <String>['/health', 'telemetry.example.com'],
disableInDevelopment: true,
),
);
OptionDefaultPurpose
apiUrlRejourney cloudBase URL for cloud or self-hosted ingestion.
enabledtrueLocal SDK switch.
observeOnlyfalseCollect telemetry without visual capture.
captureFpsremote/defaultLocal upper capture frequency.
maxSessionDurationremote/defaultLocal maximum session duration.
stopTimeout10 secondsBounds how long stop() waits; native persistence continues after a timeout.
disableInDevelopmentfalsePrevent start in debug builds.
captureQualitymediumNative frame compression preset.
wifiOnlyfalseRestrict artifact upload to Wi-Fi.
captureScreentruePermit replay frame capture.
captureAnalyticstruePermit behavioral analytics.
captureCrashestrueEnable supported native crash handling.
captureAnrstrueEnable Android ANR observation.
trackConsoleLogstrueInclude supported diagnostic logs.
collectDeviceInfotrueInclude device and application context.
collectGeoLocationtrueInclude coarse network-derived location when supported.
autoTrackNetworktrueEnable supported request instrumentation.
captureNativeSheetstrueCapture supported native modal surfaces.
networkCaptureSizestrueInclude request/response byte sizes.

Lifecycle and Advanced API

  • Rejourney.getSessionId() returns the active identifier.
  • Rejourney.stop() flushes and closes the active session.
  • Rejourney.markVisualChange(reason) requests an immediate frame when capture policy permits it.
  • Rejourney.onScroll(offset) informs adaptive visual capture about scrolling.
  • Rejourney.onOAuthStarted, onOAuthCompleted, and onExternalUrlOpened handle external-screen boundaries.
  • Rejourney.logFeedback(rating, message) adds feedback to the timeline.
  • Rejourney.getSdkMetrics() exposes upload, retry, queue, eviction, crash, and session counters.
  • Rejourney.nativeEvents exposes native session lifecycle messages.

Verify the Integration

  1. Run the application on both an iOS simulator/device and an Android emulator/device.
  2. Accept the application's recording consent and confirm Rejourney.start() returns success: true with a session ID. A deliberately disabled test configuration should return error: disabled without network access.
  3. Navigate between named routes and emit a test event.
  4. Exercise masked inputs and inspect replay output before enabling production sampling.
  5. Consume an HTTP response through RejourneyHttpClient and confirm its request marker appears.
  6. Call getSdkMetrics() to verify the native bridge and health counters.
  7. Validate debug crash and ANR capture only in disposable builds.

The repository includes two working applications: the package example in packages/rejourney/example and a consumer-style app in examples/flutter.

Troubleshooting

  • MissingPluginException: stop the app completely, run flutter clean, fetch packages, and rebuild; hot reload cannot install a new native plugin.
  • iOS deployment target error: set the application deployment target to iOS 15.1 or newer and run pod install again.
  • Android minSdk error: set minSdk to 24 or newer.
  • No session starts: check consent flow, enabled, disableInDevelopment, the dashboard kill switch, project sample rate, and network access.
  • No route names: assign names in RouteSettings or provide routeNameResolver.
  • Network marker delayed: consume or drain the streamed response; the wrapper records its final byte count when the stream completes.
  • Sensitive content visible: wrap the exact subtree with RejourneyMask, verify native project privacy settings, and repeat replay QA before release.

License

The Flutter API, platform bridges, native core, examples, and documentation are licensed under the Apache License 2.0. See the package license and third-party notice files for details.