---
title: "How to add session replay to a Flutter app"
description: "Record a Flutter report screen with Amplitude session replay, synchronize analytics identity, and block sensitive content."
canonical: "https://amplitude.com/en-us/build/flutter-session-replay"
---

# How to add session replay to a Flutter app

Record a Flutter report screen with Amplitude session replay, synchronize analytics identity, and block sensitive content.

Source: https://amplitude.com/en-us/build/flutter-session-replay

---

# How to add session replay to a Flutter app

Record the report screen after permission, match it to analytics events, and end recording when the report closes. Allow about 25 minutes.

- Flutter 3.29.2+
- iOS and Android
- Replay SDK 1.0.1
- 25 min

Chris Tam · Last updated 22 October 2026

A Flutter report can look correct in an event stream while the person using it still gets stuck. I use [session replay](https://amplitude.com/session-replay) to inspect that report interaction alongside its analytics events.

In this tutorial, I extend [Flutter product analytics](https://amplitude.com/build/flutter-product-analytics) with a report-only recording window, synchronize the two SDKs' identifiers, and block a private note from capture.

Home keeps recording off. An explicit switch allows recording of the report screen. The report starts capture with the current analytics device and session IDs, uses conservative masking, and tears down its capture lifecycle on Back or backgrounding.

## Prerequisites

- The [Flutter analytics report app](https://amplitude.com/build/flutter-product-analytics), with its one `AppAnalytics` client, named screen observer and existing `ReportScreen`. Keep the verified authentication adapter and analytics module.
- Flutter 3.29.2 or later, Dart 3.7.2 or later, iOS 13+, or Android minSdk 21+ with compileSdk 36. If the app also uses Experiment 1.0.0, retain that page's higher Flutter 3.32/Dart 3.8 floor. Keep compatible generated native toolchain settings.
- The same project API key and region used by analytics. Enable project capture and a nonzero sample rate in [Session Replay settings](https://amplitude.com/docs/session-replay/session-replay-settings). Configure the required project masking level before capturing.

[Sign up for free](https://app.amplitude.com/signup) if you need an Amplitude account. Check your project's session replay entitlement and capture allowance before a production rollout.

## 1. Install the stable replay package

Use the current stable package from the [Flutter replay reference](https://amplitude.com/docs/sdks/session-replay/session-replay-flutter-standalone-sdk#install-and-initialize):

**Terminal, existing project root**

Terminal, existing project root

```sh
flutter pub add amplitude_session_replay:1.0.1
```

For iOS, retain the existing platform declaration of at least 13.0 and refresh pods. For Android, set the existing app module's minimum and compile SDK values without replacing its other Gradle settings.

**ios/Podfile, existing platform declaration**

ios/Podfile, existing platform declaration

```ruby
platform :ios, '13.0'
```

**Terminal, project root**

Terminal, project root

```sh
cd ios
pod install
cd ..
```

**android/app/build.gradle.kts, values inside the existing android block**

android/app/build.gradle.kts, values inside the existing android block

```kotlin
compileSdk = 36
// Inside defaultConfig:
minSdk = 21
```

Standard Flutter apps use automatic plugin registration. For an embedded/add-to-app integration, follow the separate [registration instructions](https://amplitude.com/docs/sdks/session-replay/session-replay-flutter-standalone-sdk#plugin-registration) before creating platform views.

## 2. Configure the report capture lifecycle

In Amplitude's project settings, enable session capture, choose a sample rate above zero and set the masking level required by your app. Remote project settings take precedence over local sampling and masking, so set conservative masking in the project as well when that is the required policy.

Create the complete service below. It takes the project key and region directly from analytics, validates active identifiers, and uses `SessionReplay.instance` rather than constructing a second recorder.

**lib/replay\_capture.dart**

lib/replay\_capture.dart

```dart
import 'package:amplitude_flutter/constants.dart' as analytics;
import 'package:amplitude_session_replay/amplitude_session_replay.dart'
    as replay;
import 'analytics.dart';

class ReportReplay {
  ReportReplay(this.analyticsClient);
  final AppAnalytics analyticsClient;
  bool _initialized = false;
  Future<void> _pending = Future<void>.value();

  Future<void> _serial(Future<void> Function() work) {
    final operation = _pending.then((_) => work());
    _pending = operation.catchError((Object _) {});
    return operation;
  }

  Future<void> start({required bool allowed}) => _serial(() async {
    if (!allowed) {
      await _dispose();
      return;
    }
    final client = analyticsClient.client;
    final deviceId = await client.getDeviceId();
    final sessionId = await client.getSessionId();
    if (deviceId == null ||
        deviceId.length < 6 ||
        sessionId == null ||
        sessionId <= 0) {
      await _dispose();
      return;
    }
    if (_initialized) {
      await replay.SessionReplay.instance.stop();
      await replay.SessionReplay.instance.setDeviceId(deviceId);
      await replay.SessionReplay.instance.setSessionId(sessionId);
      if (await replay.SessionReplay.instance.deviceId() != deviceId ||
          await replay.SessionReplay.instance.sessionId() != sessionId)
        return;
    } else {
      replay.SessionReplay.instance.init(
        replay.SessionReplayConfig(
          apiKey: client.configuration.apiKey,
          deviceId: deviceId,
          sessionId: sessionId,
          serverZone:
              client.configuration.serverZone == analytics.ServerZone.eu
                  ? replay.ServerZone.eu
                  : replay.ServerZone.us,
          privacyConfig: replay.PrivacyConfig.conservative,
        ),
      );
      _initialized = true;
    }
    await replay.SessionReplay.instance.start();
  });

  // End this report's capture lifecycle before leaving or changing identity.
  Future<void> end() => _serial(_dispose);

  Future<void> _dispose() async {
    if (!_initialized) return;
    await replay.SessionReplay.instance.dispose();
    _initialized = false;
  }
```

`start` requests recording; it does not prove that frames have been captured or uploaded. The local sample rate stays at its default while the project's remote configuration controls selection. A completed method future is not a processed replay receipt.

**Why.** Analytics can change the device ID at logout and the session ID at a session boundary. Recording must use the current pair. The service stops before updating either ID and reads the values back before restarting. A report that closes ends this capture lifecycle with `dispose`; the next opening initializes it again with current IDs.

## 3. Wrap the existing report screen

Add a wrapper that starts after the first report frame, tears down when the app backgrounds, and waits for teardown before a normal Back navigation. It also reads a changed permission value when its parent rebuilds it.

**lib/report\_replay\_window\.dart**

lib/report\_replay\_window\.dart

```dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'replay_capture.dart';

class ReportReplayWindow extends StatefulWidget {
  const ReportReplayWindow({
    super.key,
    required this.capture,
    required this.allowed,
    required this.child,
  });
  final ReportReplay capture;
  final bool allowed;
  final Widget child;
  @override
  State<ReportReplayWindow> createState() => _ReportReplayWindowState();
}

class _ReportReplayWindowState extends State<ReportReplayWindow>
    with WidgetsBindingObserver {
  bool _leaving = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) unawaited(widget.capture.start(allowed: widget.allowed));
    });
  }

  @override
  void didUpdateWidget(covariant ReportReplayWindow oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.allowed != widget.allowed) {
      unawaited(widget.capture.start(allowed: widget.allowed));
    }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed && !_leaving) {
      unawaited(widget.capture.start(allowed: widget.allowed));
    } else {
      unawaited(widget.capture.end());
    }

  Future<void> _leave(bool didPop, Object? result) async {
    if (didPop || _leaving) return;
    _leaving = true;
    await widget.capture.end();
    if (!mounted) return;
    setState(() {});
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) Navigator.of(context).pop(result);
    });
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    unawaited(widget.capture.end());
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => PopScope(
    canPop: _leaving,
    onPopInvokedWithResult: _leave,
    child: widget.child,
  );
}
```

Add these imports to the existing `lib/main.dart`. Retain the current analytics imports and any Experiment or engagement imports.

**lib/main.dart, added imports**

lib/main.dart, added imports

```dart
import 'package:amplitude_session_replay/amplitude_session_replay.dart' as replay;
import 'replay_capture.dart';
import 'report_replay_window.dart';
```

The following complete root class demonstrates the switch and route wrapper. Add it beside the existing `ReportScreen` in the base analytics page. Retain that screen, its one preview event, and all application-specific routes. After existing analytics/auth initialization, change only `runApp` to use `ReplayReportsApp`.

**lib/main.dart, root invocation after existing initialization**

lib/main.dart, root invocation after existing initialization

```dart
runApp(ReplayReportsApp(analytics: analytics));
```

**lib/main.dart, add the root class; retain existing ReportScreen**

lib/main.dart, add the root class; retain existing ReportScreen

```dart
class ReplayReportsApp extends StatefulWidget {
  const ReplayReportsApp({super.key, required this.analytics});
  final AppAnalytics analytics;
  @override
  State<ReplayReportsApp> createState() => _ReplayReportsAppState();
}

class _ReplayReportsAppState extends State<ReplayReportsApp> {
  late final capture = ReportReplay(widget.analytics);
  bool allowed = false;
  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'Usage reports',
    navigatorObservers: [AmplitudeNavigatorObserver(widget.analytics.client)],
    routes: {
      '/':
          (context) => Scaffold(
            appBar: AppBar(title: const Text('Usage reports')),
            body: Column(
              children: [
                SwitchListTile(
                  title: const Text('Allow recording of the report screen'),
                  value: allowed,
                  onChanged: (value) => setState(() => allowed = value),
                ),
                FilledButton(
                  onPressed: () => Navigator.pushNamed(context, '/report'),
                  child: const Text('Preview report'),
                ),
              ],
            ),
      '/report':
          (context) => ReportReplayWindow(
            capture: capture,
            allowed: allowed,
            child: Stack(
              children: [
                ReportScreen(analytics: widget.analytics),
                // Explicit blocking also protects sensitive images in this subtree.
                const Positioned(
                  bottom: 24,
                  left: 24,
                  child: replay.AmpBlock(child: Text('Private report note')),
                ),
              ],
            ),
    },
  );
}
```

The private note demonstrates `AmpBlock`. Wrap sensitive images and other sensitive subtrees in your actual report, because images do not receive automatic masking. Conservative text masking and explicit blocking serve different parts of the screen.

If flags are already installed, keep their report-opening decision and wrap the selected report widget in `ReportReplayWindow` inside the existing route builder. Keep its `/report` route name, retained variant and save action. Do not replace the flagged report with the base screen or add a second analytics observer.

## 4. Connect the existing consent and identity boundaries

The switch controls only this sample's replay window. In an application with existing consent, supply the current replay permission to `allowed` and await `capture.end()` when permission is revoked. Apply analytics consent to analytics separately. Keep restricted screens outside this report capture window.

Add an identity boundary so capture is torn down before switching accounts:

**lib/replay\_identity.dart**

lib/replay\_identity.dart

```dart
import 'replay_capture.dart';

// Return to Home and close engagement before calling this shared auth boundary.
Future<void> changeIdentityOutsideReport(
  ReportReplay capture,
  Future<void> Function() updateIdentity,
) async {
  await capture.end();
  await updateIdentity();
  // The next report opening reads the new analytics IDs before starting capture.
}
```

Return Home and close guides and surveys before invoking this boundary. Pass the application's existing async identity operation. For the analytics-only app, sign-out uses `analytics.signedOut`; an app with flags passes its coordinated reset/clear/refetch operation instead. The next report opening reads the new device/session pair. Do not call reset on an ordinary anonymous launch.

**lib/main.dart or existing auth handler, after returning Home**

lib/main.dart or existing auth handler, after returning Home

```dart
await changeIdentityOutsideReport(capture, analytics.signedOut);
```

Add `import 'replay_identity.dart';` to the file that uses this handler. Keep real authentication and authorization in the app. A call to the analytics helper is not an application logout.

**MCP.** Use the [Amplitude MCP server](https://amplitude.com/docs/amplitude-ai/amplitude-mcp) to inspect the authorized project's report events and identifiers while checking matching. Use replay playback to verify the recording itself.

## 5. Run and inspect a report recording

Build a fresh native target after adding the plugin. The project key is the same build-time value used by the base analytics app:

**Terminal, project root**

Terminal, project root

```sh
flutter run --dart-define=AMPLITUDE_API_KEY=your-project-api-key
```

Pass `--dart-define=AMPLITUDE_SERVER_ZONE=EU` for an EU project. Restart after changing build-time configuration or native plugin dependencies.

## Verify it worked

1. Open the report with the recording switch off. Confirm the report works without starting a replay lifecycle.
2. Return Home, allow recording and reopen the report. Interact with the report, then use Back. Check SDK diagnostics for initialization, the active IDs and upload errors.
3. Confirm the analytics report event uses the same top-level device and session IDs as replay. Both SDKs must use the same project and region; a matching user ID alone is insufficient.
4. In Amplitude, open the processed session replay associated with that test session. Confirm the report interaction appears. Check text masking and the blocked private note in playback, then verify sensitive image subtrees you added.
5. Background the app, return, and open or resume the report. Check the current analytics session ID before capture resumes. Test an actual account switch through the app's auth handler and confirm the new report uses the new device identity.
6. Revoke permission through the application's consent flow and confirm recording ends. Verify analytics consent separately if that flow controls both products.

## Troubleshooting

### The SDK starts but no replay appears

Check the project's capture toggle, sampling, entitlement, current IDs and region. The local default sample rate is zero and remote project settings control selection in this example. Confirm actual upload and processed playback rather than interpreting `start()` completion as success.

### Events and replay do not match

Use the same project key, server zone, device ID and session ID. Await teardown through the auth boundary, then reopen the report. Read IDs again when the app resumes; do not save one device/session pair permanently at startup.

### Private text or images remain visible

Remote masking can override the local level. Set the project policy correctly and inspect playback. Images need an explicit `AmpBlock`; text masking alone does not hide their content.

### Hot reload does not attach the plugin

Stop and relaunch a native build after installing native dependencies. Preserve the iOS Podfile and compatible Android SDK settings. Embedded Flutter engines also need the documented plugin registration before platform views are created.

### A report never starts recording after startup

The service deliberately declines to start without a device ID of at least six characters and a positive analytics session ID. Let analytics establish the active session and open the report again. Inspect initialization diagnostics if IDs remain unavailable.

## FAQ

### Does the example record the entire app?

It records only the report window after permission. Home and screens outside that wrapper do not start this capture lifecycle. Extend the boundary deliberately if other screens need recording.

### Is a completed flush a replay link?

A flush requests upload of pending data. Inspect SDK errors and wait for processed playback in Amplitude to prove that a recording is usable.

### Can I use this from a native host application?

This page uses Flutter-owned `init` and `start`. Native-host/add-to-app integrations use the documented `attach` lifecycle and registration requirements; keep the host as the recording owner in that setup.

## Full code reference

Unchanged from [Flutter product analytics](https://amplitude.com/build/flutter-product-analytics); keep your versions: `lib/analytics.dart`, `lib/auth.dart`, the existing `ReportScreen` implementation, and native platform configuration. The `lib/main.dart` excerpt below contains added imports, the changed root invocation and the new root class. Retain the rest of the file and merge any existing flags or engagement additions.

Full code reference

```dart
// -- lib/replay_capture.dart --
import 'package:amplitude_flutter/constants.dart' as analytics;
import 'package:amplitude_session_replay/amplitude_session_replay.dart'
    as replay;
import 'analytics.dart';

class ReportReplay {
  ReportReplay(this.analyticsClient);
  final AppAnalytics analyticsClient;
  bool _initialized = false;
  Future<void> _pending = Future<void>.value();

  Future<void> _serial(Future<void> Function() work) {
    final operation = _pending.then((_) => work());
    _pending = operation.catchError((Object _) {});
    return operation;
  }

  Future<void> start({required bool allowed}) => _serial(() async {
    if (!allowed) {
      await _dispose();
      return;
    }
    final client = analyticsClient.client;
    final deviceId = await client.getDeviceId();
    final sessionId = await client.getSessionId();
    if (deviceId == null ||
        deviceId.length < 6 ||
        sessionId == null ||
        sessionId <= 0) {
      await _dispose();
      return;
    }
    if (_initialized) {
      await replay.SessionReplay.instance.stop();
      await replay.SessionReplay.instance.setDeviceId(deviceId);
      await replay.SessionReplay.instance.setSessionId(sessionId);
      if (await replay.SessionReplay.instance.deviceId() != deviceId ||
          await replay.SessionReplay.instance.sessionId() != sessionId)
        return;
    } else {
      replay.SessionReplay.instance.init(
        replay.SessionReplayConfig(
          apiKey: client.configuration.apiKey,
          deviceId: deviceId,
          sessionId: sessionId,
          serverZone:
              client.configuration.serverZone == analytics.ServerZone.eu
                  ? replay.ServerZone.eu
                  : replay.ServerZone.us,
          privacyConfig: replay.PrivacyConfig.conservative,
        ),
      );
      _initialized = true;
    }
    await replay.SessionReplay.instance.start();
  });

  // End this report's capture lifecycle before leaving or changing identity.
  Future<void> end() => _serial(_dispose);

  Future<void> _dispose() async {
    if (!_initialized) return;
    await replay.SessionReplay.instance.dispose();
    _initialized = false;
  }

// -- lib/report_replay_window.dart --
import 'dart:async';
import 'package:flutter/material.dart';
import 'replay_capture.dart';

class ReportReplayWindow extends StatefulWidget {
  const ReportReplayWindow({
    super.key,
    required this.capture,
    required this.allowed,
    required this.child,
  });
  final ReportReplay capture;
  final bool allowed;
  final Widget child;
  @override
  State<ReportReplayWindow> createState() => _ReportReplayWindowState();
}

class _ReportReplayWindowState extends State<ReportReplayWindow>
    with WidgetsBindingObserver {
  bool _leaving = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) unawaited(widget.capture.start(allowed: widget.allowed));
    });
  }

  @override
  void didUpdateWidget(covariant ReportReplayWindow oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.allowed != widget.allowed) {
      unawaited(widget.capture.start(allowed: widget.allowed));
    }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed && !_leaving) {
      unawaited(widget.capture.start(allowed: widget.allowed));
    } else {
      unawaited(widget.capture.end());
    }

  Future<void> _leave(bool didPop, Object? result) async {
    if (didPop || _leaving) return;
    _leaving = true;
    await widget.capture.end();
    if (!mounted) return;
    setState(() {});
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) Navigator.of(context).pop(result);
    });
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    unawaited(widget.capture.end());
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => PopScope(
    canPop: _leaving,
    onPopInvokedWithResult: _leave,
    child: widget.child,
  );
}

// -- lib/replay_identity.dart --
import 'replay_capture.dart';

// Return to Home and close engagement before calling this shared auth boundary.
Future<void> changeIdentityOutsideReport(
  ReportReplay capture,
  Future<void> Function() updateIdentity,
) async {
  await capture.end();
  await updateIdentity();
  // The next report opening reads the new analytics IDs before starting capture.
}

// -- lib/main.dart: add these imports; keep existing imports --
import 'package:amplitude_session_replay/amplitude_session_replay.dart' as replay;
import 'replay_capture.dart';
import 'report_replay_window.dart';

// -- lib/main.dart: use this root after existing analytics/auth initialization --
// runApp(ReplayReportsApp(analytics: analytics));

// -- lib/main.dart: add this root; retain the existing ReportScreen class --
class ReplayReportsApp extends StatefulWidget {
  const ReplayReportsApp({super.key, required this.analytics});
  final AppAnalytics analytics;
  @override
  State<ReplayReportsApp> createState() => _ReplayReportsAppState();
}

class _ReplayReportsAppState extends State<ReplayReportsApp> {
  late final capture = ReportReplay(widget.analytics);
  bool allowed = false;
  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'Usage reports',
    navigatorObservers: [AmplitudeNavigatorObserver(widget.analytics.client)],
    routes: {
      '/':
          (context) => Scaffold(
            appBar: AppBar(title: const Text('Usage reports')),
            body: Column(
              children: [
                SwitchListTile(
                  title: const Text('Allow recording of the report screen'),
                  value: allowed,
                  onChanged: (value) => setState(() => allowed = value),
                ),
                FilledButton(
                  onPressed: () => Navigator.pushNamed(context, '/report'),
                  child: const Text('Preview report'),
                ),
              ],
            ),
      '/report':
          (context) => ReportReplayWindow(
            capture: capture,
            allowed: allowed,
            child: Stack(
              children: [
                ReportScreen(analytics: widget.analytics),
                // Explicit blocking also protects sensitive images in this subtree.
                const Positioned(
                  bottom: 24,
                  left: 24,
                  child: replay.AmpBlock(child: Text('Private report note')),
                ),
              ],
            ),
    },
  );
}
```

## Hand it to your agent

**Coding-agent prompt**

Coding-agent prompt

```text
Extend the existing Flutter report app using https://amplitude.com/build/flutter-session-replay.
Use amplitude_session_replay 1.0.1 with Flutter >=3.29.2, Dart >=3.7.2, iOS >=13 or Android minSdk 21/compileSdk 36. Retain higher runtime floors from other installed hub packages. Keep the existing analytics client, auth adapter, report preview event and named screen observer.
Use SessionReplay.instance, initialize synchronously once per capture lifecycle, and call start only after replay permission and valid current analytics IDs. Reuse the analytics project key and region. Keep remote configuration and configure nonzero project sampling plus the required masking policy in Amplitude.
Add ReportReplay and ReportReplayWindow. Capture only the report, with Home initially off. Tear down the report lifecycle on Back/backgrounding and reinitialize with current IDs on the next opening or resume. On an active resync stop, update device/session IDs and verify matching values before restarting.
Use conservative masking and AmpBlock for private subtrees including sensitive images. Inspect actual playback; future completion is not recording or ingestion proof. Apply consent separately to Analytics and Replay.
Preserve the selected flag layout and save action if installed. Wrap the existing report widget rather than replacing it with another hub's screen. Keep route names, observers, imports and every unrelated application route.
At identity changes, return Home, close engagement if installed, end replay capture, then run the existing auth/analytics reset and flag clear/refetch coordinator. The next report reads fresh IDs. Keep actual logout separate from analytics reset.
Verify no recording before permission, both IDs and region, foreground/background and account transitions, masking and processed replay playback. Report all files changed and exactly which checks ran.
```

## What this unlocks

[Session replay](https://amplitude.com/session-replay): inspect the interaction behind a report preview.

[Product analytics](https://amplitude.com/amplitude-analytics): connect the report event to a recording using the same device and session.

[Experimentation](https://amplitude.com/amplitude-experiment): inspect report interactions while retaining the layout selected by the existing flag flow.

## Next steps

[Web frameworksFlutter product analyticsReview the shared client and identity foundation.](https://amplitude.com/build/flutter-product-analytics)[Web frameworksFlutter feature flags and experimentsKeep the chosen report layout and measure saves after exposure.](https://amplitude.com/build/flutter-feature-flags)[Web frameworksFlutter guides and surveysHelp users on the named report screen.]()

Reference docs

[Flutter Session Replay →](https://amplitude.com/docs/sdks/session-replay/session-replay-flutter-standalone-sdk)[Flutter Analytics SDK →](https://amplitude.com/docs/sdks/analytics/flutter/flutter-sdk-4)[Session Replay settings →](https://amplitude.com/docs/session-replay/session-replay-settings)[Privacy settings →](https://amplitude.com/docs/session-replay/manage-privacy-settings-for-session-replay)[Session matching →](https://amplitude.com/docs/session-replay/session-matching)[Amplitude MCP server →](https://amplitude.com/docs/amplitude-ai/amplitude-mcp)

For agents

[View raw .md](/build/flutter-session-replay.md)[llms.txt](/llms.txt)

## Inspect the report interaction

Open a report recording and compare what happened on screen with the events in the same session.

[Create a free account](https://app.amplitude.com/signup)[Browse all Build tutorials](https://amplitude.com/build)
