> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mixpanel.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Mixpanel SDKs: Flutter

## Getting Started

Please refer to our [Quickstart Guide](/docs/quickstart/install-mixpanel#flutter).

The [Full API Reference](https://mixpanel.github.io/mixpanel-flutter), [Library Source Code](https://github.com/mixpanel/mixpanel-flutter), and an [Example Application](https://github.com/mixpanel/mixpanel-flutter/tree/main/example) is documented in our GitHub repo.

### Apple Platform Support

Each major version of the SDK has its own minimum supported iOS and macOS versions. Version 3.x raises the minimums to match the minimum deployment targets of Xcode 27. Use this table to choose the version that fits your app.

| SDK version | Minimum supported iOS | Minimum supported macOS | Supported Xcode versions | Support status |
| - | - | - | - | - |
| 3.x | iOS 15 | macOS 12 | Xcode 26 and 27 | **Current:** receives new features and fixes |
| 2.x | iOS 13 | macOS 10.15 | Xcode 26 | **Maintenance:** receives critical fixes only, until April 2027 |

If your app uses CocoaPods, on 3.x set `platform :ios, '15.0'` in `ios/Podfile` and `platform :osx, '12.0'` in `macos/Podfile`. Building with Xcode 27 requires Flutter 3.47+.
If your app needs to support an older iOS or macOS version, stay on 2.x.

## Installing the Library

To install the library in your [Flutter development environment](https://docs.flutter.dev/get-started/install), declare it as a dependency in the `pubspec.yaml` file of your package:

```yaml theme={"system"}
# add Mixpanel library to your dependencies
dependencies:
    mixpanel_flutter: ^1.x.x # set this to your desired version
```

Once you added the library to your dependencies, use the command line to install the library:

```bash theme={"system"}
# install packages in your dependencies
$ flutter pub get
```

After the installation, import the package in your Dart code, then initialize the SDK with the [`Mixpanel.init()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/init.html) using your [project token](/docs/orgs-and-projects/managing-projects#find-your-project-tokens).

```dart theme={"system"}
// import the library
import 'package:mixpanel_flutter/mixpanel_flutter.dart';
...
class _YourClassState extends State<YourClass> {
  Mixpanel mixpanel;

  @override
  void initState() {
    super.initState();
    initMixpanel();
  }

  Future<void> initMixpanel() async {
    // initialize Mixpanel
    mixpanel = await Mixpanel.init("YOUR_PROJECT_TOKEN", trackAutomaticEvents: false);
  }
}
...
```

Now you can access `mixpanel` throughout the rest of your application.

**Flutter Web Support**

Add the following snippet to your `web/index.html` inside the `<head></head>` tag in your project:

```html theme={"system"}
<script src="./assets/packages/mixpanel_flutter/assets/mixpanel.js"></script>
```

### Library Configuration

<Warning>
  For projects with EU or India data residency, you must configure the SDK to use the correct regional endpoint. Events sent to the wrong region will not be ingested. Learn more about [Privacy-Friendly Tracking](/docs/tracking-methods/sdks/flutter#privacy-friendly-tracking).
</Warning>

The Mixpanel instance can be customized with different configurations. The [`Mixpanel.init()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/init.html) method accepts arguments that sets some configurations on your instance upon initialization.

After initialization, you can further customize the instance using the configuration options [here](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel-class.html).

**Example Usage**

```dart theme={"system"}
...
// initialize Mixpanel without autotrack disabled
mixpanel = await Mixpanel.init("YOUR_PROJECT_TOKEN", trackAutomaticEvents: false);

// enable debug logs post-init
mixpanel.setLoggingEnabled(true);
...
```

## Sending Events

Use [`.track()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/track.html) to send an event by providing the event name and any event properties. This will trigger a request to the [/track API endpoint](/reference/track-event) to ingest the event into your project.

<Note>
  The [/track endpoint](/reference/track-event) will only validate events with timestamps within the last 5 days of the request. Events with timestamps older than 5 days will not be ingested. See below on best practices for historical imports.
</Note>

**Example Usage**

```dart theme={"system"}
//Track 'some_event' with "plan" event prop
mixpanel.track('some_event', {'plan': 'premium'});
```

### Timing Events

You can track the time it took for an action to occur, such as an image upload or a comment post, using [`.timeEvent()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/timeEvent.html). This will mark the "start" of your action, which will be timed until you finish with a track call. The time duration is then recorded in the "Duration" property.

```dart theme={"system"}
// start the timer for the event "Image Upload"
mixpanel.timeEvent("Image Upload");

// 20 seconds later...

// track "Image Upload" event with Duration event prop set to 20
mixpanel.track("Image Upload");
```

### Flushing Events

To preserve battery life and customer bandwidth, the Mixpanel library doesn’t send the events you record immediately. Instead, it sends batches to the Mixpanel servers every 60 seconds while your application is running, as well as when the application transitions to the background.

Call [`.flush()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/flush.html) manually if you want to force a flush at a particular moment.

**Example Usage**

```dart theme={"system"}
// flush batched events for ingestion immediately
mixpanel.flush();
```

#### Importing Historical Events

The Flutter SDK is a tracking SDK designed for real-time tracking in a client-side environment. Calling `.track()` triggers a request to our [/track API endpoint](/reference/track-event), which will validate for events with a timestamp that is within the last 5 days of the request. **Events older than 5 days will not be ingested**.

For bulk import of historical events older than 5 days, we will need to use the [/import API endpoint](/reference/import-events) which is optimized for scripting and supports ingesting historical data. We recommend the [Python SDK](/docs/tracking-methods/sdks/python) (see the [`.import_data()`](https://mixpanel.github.io/mixpanel-python/#primary-interface) function) and [mixpanel-utils module](https://github.com/mixpanel/mixpanel-utils) (see the [`import_events()`](https://github.com/mixpanel/mixpanel-utils?tab=readme-ov-file#import-events) function) which both leverages the /import API for event ingestion.

## Setting Super Properties

Super properties are global event properties that you define once and apply to all events.

To register super properties, call [`.registerSuperProperties()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/registerSuperProperties.html).

Use [`.registerSuperPropertiesOnce()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/registerSuperPropertiesOnce.html) to register super properties without overwriting existing values.

**Example Usage**

```dart theme={"system"}
// register a "name" super property
mixpanel.registerSuperProperties({'name': 'sam'});

// track "some_event"
// "name" is automatically added as an event prop
mixpanel.track('some_event');

// this is ignored because "name" already exists
mixpanel.registerSuperPropertiesOnce({'name': 'samantha'});

// register a "location" super property
mixpanel.registerSuperPropertiesOnce({'location': 'us'});
```

Our mobile libraries store your super properties in local storage. They will persist so long as the app is installed (between launches and updates). Uninstalling the app will remove that customers super properties.

See more methods related to super properties in the complete library reference [here](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel-class.html).

## Managing User Identity

You can handle the identity of a user using the [`.identify()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/identify.html) and [`.reset()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/reset.html) methods. Learn more about [identity management](/docs/tracking-methods/id-management) and [identifying users](/docs/tracking-methods/id-management/identifying-users-simplified).

### Identify

<Warning>
  We recommend against calling `.identify()` for anonymous visitors to your site.
</Warning>

Call [`.identify()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/identify.html) when you know the identity of the current user, passing in their user ID as an argument. This is typically at account registration and at log in.

**Example Usage**

```dart theme={"system"}
// user logs in and tracks a sign in event
mixpanel.track('sign in');

// upon sign in, identify the user with their ID
// ensure future events sent from the user have distinct_id 12345
mixpanel.identify('12345');
```

### Call Reset at Logout

Call [`.reset()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/reset.html) to clear data attributed to a user when they logout. This will clear the local storage and allows you to handle [multiple users on a single device](/docs/tracking-methods/id-management/identifying-users-simplified#multiple-users-one-device).

**Example Usage**

```dart theme={"system"}
// your user logs out and tracks a log out event
mixpanel.track('log out');

// clear local storage and generate new distinct_id
mixpanel.reset();
```

## Storing User Profiles

Once your users are identified, create [user profiles](/docs/data-structure/user-profiles) by setting profile properties to describe them. Example profile properties include "name", "email", "company", and any other demographic details about the user.

The Flutter SDK provides a few methods for setting profile properties under the [`People`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/People-class.html) class accessible via [`.getPeople()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/getPeople.html). These methods will trigger requests to the [/engage API endpoint](/reference/profile-set).

### Setting Profile Properties

<Note>
  You must call `.identify()` before setting profile properties in order to associate the profile properties you set with the target user. If identify is not called, the profile update will be queued for ingestion until an identify call is made.
</Note>

Set profile properties on a user profile by calling the [`.getPeople().set()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/People/set.html) method.

If a profile property already exists, it will be overwritten with the latest value provided in the method. If a profile property does not exist, it will be added to the profile.

**Example Usage**

```dart theme={"system"}
// You must call identify to associate the profile update with the user
// Sets user's "Plan" attribute to "Premium"
mixpanel.identify("12345");
mixpanel.getPeople().set("Plan", "Premium");

// Update "plan" from "Premium" to "Enterprise"
mixpanel.getPeople().set("Plan", "Enterprise");
```

### Other Types of Profile Updates

There are a few other methods for setting profile properties. See a complete reference of the available methods [here](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/People-class.html)

A few commonly used people methods are highlighted below:

<Tabs>
  <Tab title=".setOnce()">
    The [`.getPeople().setOnce()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/People/setOnce.html) method set profile properties only if they do not exist yet. If it is setting a profile property that already exists, it will be ignored.

    Use this method if you want to set profile properties without the risk of overwriting existing data.

    **Example Usage**

    ```dart theme={"system"}
    // set profile properties for user "1234"
    mixpanel.identify('1234');
    mixpanel.getPeople().set('name':'sam');

    // will be ignored since "name" already exists
    mixpanel.getPeople().setOnce('name', 'samantha');

    // set "location" user prop since it does not exist
    mixpanel.getPeople().setOnce('location', 'us');
    ```
  </Tab>

  <Tab title=".append()">
    The [`.getPeople().append()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/People/append.html) method append values to a list profile property.

    Use this method to add additional values to an existing list property instead of redefining the entire list.

    **Example Usage**

    ```dart theme={"system"}
    // set profile properties for user "1234"
    mixpanel.identify('1234');
    mixpanel.getPeople().set({
        'name':'sam',
        'roles': ['sales','engineer']
    });

    // add "legal" to "roles"  
    // new role values are ['sales','engineer','legal']
    mixpanel.getPeople().append('roles', 'legal');

    // .append() allows duplicates
    // new "roles" values are ['sales','engineer','legal', 'legal']
    mixpanel.getPeople().append('roles', 'legal');
    ```
  </Tab>

  <Tab title=".union()">
    The [`.getPeople().union()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/People/union.html) method append new values to a list property, excluding duplicates.

    Use this method to create a list profile property that only contains unique values without duplicates.

    **Example Usage**

    ```dart theme={"system"}
    // set profile properties for user "1234"
    mixpanel.identify('1234');
    mixpanel.getPeople().set({
        'name':'sam',
        'roles': ['sales','engineer']
    });

    // append "engineer" to "roles"
    // will be ignored since "engineer" already exists in "roles"
    mixpanel.getPeople().union('roles', 'engineer');

    // append "legal" to "roles"
    // new role values are ['sales','engineer','legal']
    mixpanel.getPeople().union('roles', 'legal');
    ```
  </Tab>

  <Tab title=".increment()">
    The [`.getPeople().increment()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/People/increment.html) method increments a numeric property by a whole number.

    Use this method to add to or subtract from your numeric property based on its current value.

    **Example Usage**

    ```dart theme={"system"}
    // set profile properties for user "1234"
    mixpanel.identify('1234');
    mixpanel.getPeople().set({
        'name':'sam',
        'age': 25
    });

    // increment "age" by 2
    mixpanel.getPeople().increment('age',2);

    // use negative number to decrement
    // decrement "age" by 5
    mixpanel.getPeople().increment('age',-5);
    ```
  </Tab>
</Tabs>

## Group Analytics

<Note>
  Read more about [Group Analytics](/docs/data-structure/group-analytics) before proceeding. You will need to have the [group key defined in your project settings](/docs/data-structure/group-analytics#group-keys-in-project-settings) first.
</Note>

Mixpanel Group Analytics is a paid add-on that allows behavioral data analysis by selected groups, as opposed to individual users.

A group is identified by the `group_key` and `group_id`.

* `group_key` is the event property that connects event data to a group. (e.g. `company`)
* `group_id` is the identifier for a specific group. (e.g. `mixpanel`,`company_a`,`company_b`, etc.)

The Android SDK provides a few method for adding individual users to a group and setting group profile properties.

### Adding Users to a Group

[All events must have the group key as an event property in order to be attributed to a group](/docs/data-structure/group-analytics/group-analytics-implementation#tracking-events-for-a-group). Without the group key, an event cannot be attributed to a group.

Call the [`.setGroup()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/setGroup.html) method to register the current user to a group, which would add the `group_key` as an event property set to the `group_id` value to all events moving forward.

**Example Usage**

```dart theme={"system"}
// assign the current user to the "mixpanel" company group
mixpanel.setGroup('company', 'mixpanel');
 
// track "some_event"
// event property "company" = ["mixpanel"] is added automatically
mixpanel.track('some_event');

//alternatively you can manually define the group key
mixpanel.track('some_event', {'company':'mixpanel'});
```

**Multiple Groups**

[An event can be attributed to multiple groups](/docs/data-structure/group-analytics#attribute-events-to-multiple-groups) by passing in the `group_key` value as a list of multiple `group_id` values.

Call [`.addGroup()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/addGroup.html) to add additional `group_id`s to an existing list.

**Example Usage**

```dart theme={"system"}
// assign the current user to the "mixpanel" company group
// events will contain 'company' prop set to ["mixpanel"]
mixpanel.setGroup('company', 'mixpanel');

// add "mp-us" as an additional company group
// new "company" value is ["mixpanel","mp-us"]
mixpanel.addGroup('company', 'mp-us');

// track "some_event"
// event property "company" = ["mixpanel","mp-us"] is added automatically
mixpanel.track('some_event');
```

### Adding Group Identifiers to User Profiles

To connect group information to a user profile, include the `group_key` and `group_id` as a user profile property using the `.getPeople().set()` call.

**Example Usage**

```dart theme={"system"}
// set group key "company" as user prop
// with group id "mixpanel" as the value
mixpanel.identify("12345");
mixpanel.getPeople().set("company", "mixpanel");
```

### Setting Group Profile Properties

Create a group profiles by setting group properties, similar to a user profile. For example, you may want to describe a company group with properties such as "ARR", "employee\_count", and "subscription".

To set group profile properties, specify the group that needs to be updated by calling [`.getGroup()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/getGroup.html), then set the group properties by chaining the [`.set()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/MixpanelGroup/set.html) method, which will trigger a request to the [/groups API endpoint](/reference/group-set-property).

**Example Usage**

```dart theme={"system"}
// assign the current user to the "mixpanel" company group
mixpanel.setGroup('company', 'mixpanel');

// specify the target group using the group_key and group_id
// set "industry" as a group profile prop to "analytics"
mixpanel.getGroup('company','mixpanel').set('industry','analytics');
```

### Other Group Profile Methods

See all of the methods under the Group class [here](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/MixpanelGroup-class.html).

A few commonly used group methods are highlighted below:

<Tabs>
  <Tab title=".setOnce()">
    The [`.getGroup().setOnce()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/MixpanelGroup/setOnce.html) method set group profile properties only if they do not exist yet. If it is setting a profile property that already exists, it will be ignored.

    Use this method if you want to set group profile properties without the risk of overwriting existing data.

    **Example Usage**

    ```dart theme={"system"}
    // assign the current user to the "mixpanel" company group
    mixpanel.setGroup('company', 'mixpanel');

    // set group profile properties
    mixpanel.getGroup('company','mixpanel').set('name', 'Mixpanel');

    // ignored since "name" is already exists
    mixpanel.getGroup('company','mixpanel').setOnce('name','mp');

    // set "location" group prop since it does not exist
    mixpanel.getGroup('company','mixpanel').setOnce('location','us');
    ```
  </Tab>

  <Tab title=".unset()">
    The [`.getGroup().unset()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/MixpanelGroup/unset.html) method removes a group property from a group profile.

    Use this method to delete unwanted group profile properties from a specific group profile.

    **Example Usage**

    ```dart theme={"system"}
    // assign the current user to the "mixpanel" company group
    mixpanel.setGroup('company', 'mixpanel');

    // set group profile properties
    mixpanel.getGroup('company','mixpanel').set('name', 'Mixpanel');
    mixpanel.getGroup('company','mixpanel').set('employee_count', 100);

    // delete "employee_count" from the group profile
    mixpanel.getGroup('company','mixpanel').unset('employee_count');

    // only "name" remains as a group prop
    ```
  </Tab>

  <Tab title=".union()">
    The [`.getGroup().union()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/MixpanelGroup/union.html) method append new values to a list property, excluding duplicates.

    Use this method to create a list group profile property that only contains unique values without duplicates.

    **Example Usage**

    ```dart theme={"system"}
    // assign the current user to the "mixpanel" company group
    mixpanel.setGroup('company', 'mixpanel');

    // set group profile properties
    mixpanel.getGroup('company','mixpanel').set('features', ['reports','alerts','cohorts']);

    // add "data pipeline" to "features" prop
    // ignore "alert" since it is a duplicate value
    mixpanel.getGroup('company','mixpanel').union('features', [
        'data pipeline',
        'alerts'
        ]);
    ```
  </Tab>

  <Tab title=".remove()">
    The [`.getGroup().remove()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/MixpanelGroup/remove.html) method removes a value from a list-valued group profile property.

    Use this method to remove specific values from a list without affecting all of the other values in the list.

    **Example Usage**

    ```dart theme={"system"}
    // assign the current user to the "mixpanel" company group
    mixpanel.setGroup('company', 'mixpanel');

    // set group profile properties
    mixpanel.getGroup('company','mixpanel').set('features', ['reports','alerts','cohorts']);

    //remove "alerts" from "features"
    // "features" now contain ["reports","cohorts"]
    mixpanel.getGroup('company','mixpanel').remove('features', 'alerts');
    ```
  </Tab>
</Tabs>

## Autocapture

The Mixpanel Flutter SDK provides methods on the `autocapture` object to help you track common user interactions with minimal instrumentation. These methods fire standardized events that are recognized by the Mixpanel platform.

### Screen View & Screen Leave

Flutter apps use widget-based routing with `Navigator`, `GoRouter`, or other packages, each handling screen transitions differently. This makes it impractical for the SDK to automatically detect screen boundaries. Instead, you hook `trackScreenView` and `trackScreenLeave` into your app's navigation, giving you full control over what counts as a screen and when transitions happen.

Track screen navigations using `trackScreenView` and `trackScreenLeave`. These methods fire the following events:

| Method | Mixpanel Event |
| - | - |
| `trackScreenView()` | `$mp_page_view` |
| `trackScreenLeave()` | `$mp_page_leave` |

Both events automatically include `current_page_title` (the screen name) and `$mp_autocapture: true`.

<Tabs>
  <Tab title="Navigator">
    Create a `NavigatorObserver` and attach it to your `MaterialApp`:

    ```dart theme={"system"}
    class MixpanelNavigatorObserver extends NavigatorObserver {
      final Future<Mixpanel> _mixpanelFuture;
      MixpanelNavigatorObserver(this._mixpanelFuture);

      @override
      void didPush(Route route, Route? previousRoute) {
        super.didPush(route, previousRoute);
        _mixpanelFuture.then((mp) async {
          if (previousRoute?.settings.name != null) {
            await mp.autocapture.trackScreenLeave(previousRoute!.settings.name!);
          }
          if (route.settings.name != null) {
            await mp.autocapture.trackScreenView(route.settings.name!);
          }
        });
      }

      @override
      void didPop(Route route, Route? previousRoute) {
        super.didPop(route, previousRoute);
        _mixpanelFuture.then((mp) async {
          if (route.settings.name != null) {
            await mp.autocapture.trackScreenLeave(route.settings.name!);
          }
          if (previousRoute?.settings.name != null) {
            await mp.autocapture.trackScreenView(previousRoute!.settings.name!);
          }
        });
      }

      @override
      void didReplace({Route? newRoute, Route? oldRoute}) {
        super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
        _mixpanelFuture.then((mp) async {
          if (oldRoute?.settings.name != null) {
            await mp.autocapture.trackScreenLeave(oldRoute!.settings.name!);
          }
          if (newRoute?.settings.name != null) {
            await mp.autocapture.trackScreenView(newRoute!.settings.name!);
          }
        });
      }

      @override
      void didRemove(Route route, Route? previousRoute) {
        super.didRemove(route, previousRoute);
        _mixpanelFuture.then((mp) async {
          if (route.settings.name != null) {
            await mp.autocapture.trackScreenLeave(route.settings.name!);
          }
        });
      }
    }
    ```

    ```dart theme={"system"}
    MaterialApp(
      navigatorObservers: [MixpanelNavigatorObserver(mixpanelFuture)],
      // ...
    );
    ```
  </Tab>
</Tabs>

#### Passing Custom Properties

Both methods accept an optional `properties` parameter. For example, forwarding UTM params from a deep link:

```dart theme={"system"}
final utmProps = {
  "utm_source": uri.queryParameters["utm_source"],
  "utm_medium": uri.queryParameters["utm_medium"],
  "utm_campaign": uri.queryParameters["utm_campaign"],
};
await mixpanel.autocapture.trackScreenView("PromoScreen", properties: utmProps);
```

### Click Events

<Note>
  **Beta.** Click autocapture is in beta. Its events and properties may change before general availability. It needs Flutter SDK v2.15.0+ (Flutter 3.19+ / Dart 3.3+) and is **disabled by default**. See [Enabling Click Autocapture](#enabling-click-autocapture) to turn it on.
</Note>

The SDK can capture taps without any per-widget code. One root widget observes everything below it (routes, dialogs, bottom sheets and overlays) and works with Material and Cupertino widgets. It produces three events:

| Event | Fires when |
| - | - |
| `$mp_click` | The user taps any element |
| `$mp_dead_click` | A tap on an interactive element produces no UI change |
| `$mp_rage_click` | The user taps rapidly and repeatedly in the same spot |

Each event includes `$mp_autocapture: true` and the [Tracked Properties](#tracked-properties).

Capture runs in Dart, in your widget tree, not in the native iOS and Android SDKs. That is why it needs the root `MixpanelAutocaptureWidget` as well as the init option.

#### Click

`$mp_click` fires for every tap that resolves to a widget, whether or not that widget handles the tap. The observer listens passively and never joins Flutter's gesture arena, so it does not compete with your gesture handlers.

A tap counts only if it is:

* a **primary** touch or mouse press
* the only pointer down at the time
* released within **500 ms** (inclusive)
* inside the platform hit slop

Scrolls, drags, long presses, multi-touch, secondary buttons, stylus input (not supported in beta), keyboard activation and assistive-technology activation do not count.

**Which widget is reported:**

* A tap on a non-interactive child resolves to the nearest interactive ancestor. A tap on the `Text` inside an `ElevatedButton` reports the button, not the label.
* A nested `InkWell` inside another `InkWell` keeps its own identity.
* A button's internal `InkWell` is reported as the public button (`ElevatedButton`, not `InkWell`).
* If there is no interactive ancestor, the tapped `Text`, `RichText`, `Image` or `Icon` is reported.

#### Dead Click

`$mp_dead_click` fires when a tap on an **interactive** widget produces no visible change. The SDK snapshots the widget tree at pointer down, before your handler runs, and compares it after `DeadClickOptions.timeWindow` (default **500 ms**, [configurable](#enabling-click-autocapture)). If nothing changed, the event fires. Every dead click also has a `$mp_click` for the same tap.

A scroll, focus change, navigation, or another tap before the time window ends counts as a response, so no dead click is sent.

**Never dead-click candidates:**

* non-interactive widgets, such as a plain `Text` or a decorative `Icon`
* disabled buttons (`onPressed: null`)
* controls whose feedback the detector cannot observe: `TextField`, `EditableText`, `CupertinoTextField`, `Switch`, `CupertinoSwitch`, `Checkbox`, `Radio`, `Slider`, `RangeSlider`, `CupertinoSlider`, `CupertinoPicker`, `CupertinoDatePicker`, `DropdownButton`, `SwitchListTile`, `CheckboxListTile`, `RadioListTile`

<Note>
  Detection looks only at UI changes, not network activity. A handler that sends a request but leaves the screen unchanged for the whole window is reported as dead, which usually matches what the user saw. Only the state at the deadline is compared, so a change that appears and reverts before then is missed. Material's own ripple and border feedback does not count as a response.
</Note>

<Note>
  **Surfaces the SDK cannot inspect turn dead clicks off. They never cause one.** Automatic dead click detection is disabled for the whole view when:

  * a visible `AndroidView`, `UiKitView`, `HtmlElementView`, `PlatformViewLink`, `Texture` or app-owned `CustomPaint` is present, or
  * a snapshot cannot be taken: the tree is detached, the geometry is invalid, or the tree has more than 2,000 elements or is deeper than 512.

  In these cases the SDK sends nothing rather than guess. Use [`trackDeadClick`](#manual-click-tracking) for dead clicks your app detects on those surfaces.
</Note>

#### Rage Click

`$mp_rage_click` fires on the **4th tap** that lands within **1000 ms** and **44 logical pixels** of the earlier taps (defaults for `clickThreshold`, `timeWindow` and `radius` on `RageClickOptions`, [configurable](#enabling-click-autocapture)).

* The individual `$mp_click` events still fire, so 4 rapid taps send 4 clicks and 1 rage click.
* Tap history resets after each rage click, so 8 rapid taps send 2 rage clicks, not 5.
* Taps spread out in time or across the screen do not count.

Rage clicks and dead clicks show where your app frustrates users without throwing an error. Break them down by `$el_id` to find the problem controls, or use them to pick sessions to watch in Session Replay.

#### One-Time Setup: Element IDs

Do this once, before you enable click autocapture. It is optional (every tap is captured either way), but without it widgets report an unreadable [hash fallback](#element-id-resolution) such as `ElevatedButton_a3f21c` as their `$el_id`.

Wrap each widget you want to measure in `Semantics(identifier: ...)`. Put the wrapper on or above the widget that handles the tap, not on the `Text` or `Icon` inside it.

**Example Usage**

```dart theme={"system"}
// wrap the button, not its label
// taps on this button report $el_id = "checkout_button"
Semantics(
  identifier: 'checkout_button',
  child: ElevatedButton(
    onPressed: _checkout,
    child: const Text('Checkout'),
  ),
)
```

Use a short, static value, and do not build it from list indexes, timestamps or user data. [Element ID Resolution](#element-id-resolution) has the full rules, including what the SDK ignores.

#### Enabling Click Autocapture

You need both steps. If either is missing, nothing is captured.

**1. Pass `AutocaptureOptions` to `Mixpanel.init`.**

**Example Usage**

```dart theme={"system"}
// enable click autocapture with the default options
final mixpanel = await Mixpanel.init(
  'YOUR_PROJECT_TOKEN',
  trackAutomaticEvents: false,
  autocaptureOptions: const AutocaptureOptions(),
);
```

<Warning>
  **EU and India data residency:** If your project uses EU or India data residency, you must explicitly set the API host in your init code: add `serverURL: "https://api-eu.mixpanel.com"` (EU) or `serverURL: "https://api-in.mixpanel.com"` (India) to `Mixpanel.init`. Otherwise, your data, including autocaptured events, will be dropped. If you send data through a proxy or custom domain, point it at your region's host instead and keep the proxy URL in your init code. See [EU Data Residency](#eu-data-residency) and [India Data Residency](#india-data-residency).
</Warning>

**2. Wrap your app in `MixpanelAutocaptureWidget`**, above `MaterialApp` or `CupertinoApp` and their navigators. Anything outside its subtree is not captured. Use exactly one. A second, nested wrapper does nothing and asserts in debug builds.

**Example Usage**

```dart theme={"system"}
// wrap the app above MaterialApp so every route is observed
runApp(MixpanelAutocaptureWidget(
  instance: mixpanel,
  child: const MaterialApp(home: HomePage()),
));
```

`instance` can be `null` while async init finishes. Mount the wrapper right away and set the instance once `init` resolves. Your `child` is never remounted when capture starts.

Each signal can be configured or turned off on its own:

| Option | Setting | Default |
| - | - | - |
| `ClickOptions` | `enabled` | `true` |
| `RageClickOptions` | `enabled` | `true` |
| `RageClickOptions` | `clickThreshold`: taps required (2–100) | `4` |
| `RageClickOptions` | `timeWindow`: window the taps must fall in | `Duration(seconds: 1)` |
| `RageClickOptions` | `radius`: max distance in logical pixels (0–100000) | `44` |
| `DeadClickOptions` | `enabled` | `true` |
| `DeadClickOptions` | `timeWindow`: wait before a click counts as dead | `Duration(milliseconds: 500)` |

**Example Usage**

```dart theme={"system"}
const options = AutocaptureOptions(
  // require 5 taps within 800 ms for a rage click
  rageClickOptions: RageClickOptions(
    clickThreshold: 5,
    timeWindow: Duration(milliseconds: 800),
  ),
  // turn off dead click detection
  deadClickOptions: DeadClickOptions(enabled: false),
);
```

Both `timeWindow` values take a `Duration` from 1 ms to 1 minute (microsecond precision). Out-of-range values assert in debug builds and are clamped in release builds. A non-finite `radius` falls back to 44.

**Platforms:** Android, iOS, macOS and web. On Windows and Linux the wrapper does nothing and no events are sent.

#### Disabling Click Autocapture

Autocapture is set at init and cannot be toggled at runtime. If you omit `autocaptureOptions`, the click detectors are not set up, and a wrapper left in place just passes pointer events through. `trackScreenView` and `trackScreenLeave` still work. To turn it off in an app that already has it on, stop passing the options or disable each signal.

**Example Usage**

```dart theme={"system"}
// turn off all three click signals
const options = AutocaptureOptions(
  clickOptions: ClickOptions(enabled: false),
  rageClickOptions: RageClickOptions(enabled: false),
  deadClickOptions: DeadClickOptions(enabled: false),
);
```

Autocapture respects opt-out. Events are dropped while the user is opted out and resume after `optInTracking()`.

#### Tracked Properties

Autocaptured click, dead click and rage click events include:

| Property | Description |
| - | - |
| `$el_id` | Element identifier. See [Element ID Resolution](#element-id-resolution) |
| `$el_tag_name` | Canonical widget type, e.g. `ElevatedButton`, `InkWell` |
| `$attr-role` | Element role, e.g. `Button`, `Switch`, `Text` |
| `$elements` | Up to 5 levels of ancestor widget types, e.g. `ElevatedButton > Padding > Column` |
| `$x`, `$y` | Touch coordinates in the owning view's logical pixels |
| `$mp_autocapture` | Always `true` |

<Note>
  **`$el_tag_name` comes from a fixed list, not `runtimeType`.** The SDK knows the Material and Cupertino buttons, `InkWell`, `GestureDetector`, `Text`, `Icon`, `Image`, common layout widgets and form controls. Anything else, including your own custom widgets, is reported as `Widget`. This keeps the value stable under release-mode obfuscation. Group reports by `$el_id` and treat `$el_tag_name` as a rough hint.
</Note>

<Note>
  Element text is not captured, so there is no `$el_text` and no `$attr-aria-label`. `Semantics.label` is never reported (see [Element ID Resolution](#element-id-resolution)), and editable and secure field values are never read.
</Note>

#### Element ID Resolution

The SDK resolves `$el_id` with fixed rules that you cannot configure:

1. **`Semantics(identifier: '...')`**, read from the reported widget (see [Click](#click)) or a `Semantics` wrapper above it. The lookup never reads a child and stops at the next interactive ancestor, so an identifier on the `Text` inside a button is ignored. Blank identifiers and identifiers over 256 characters are skipped.
2. **`<WidgetType>_<hash>`**, the fallback.

**Never used as identity:**

* **`Semantics.label`.** Unlike `identifier`, which TalkBack and VoiceOver never read out, `label` is read aloud, localized, and can contain user data. The SDK never reads or reports it, so a widget with only a label falls back to the hash.
* **`Key`, `ValueKey` and `runtimeType`.**

**About the hash:** it comes from the widget's *position* in the tree (type and sibling index at each level up to the root), so it stays the same across launches and groups reliably. But it tracks a position, not a specific widget. Reordering siblings or adding a wrapper changes it, and list rows differ by index, not content.

<Warning>
  **Check your identifiers for personal data.** You set every identifier that becomes `$el_id`, and Mixpanel receives them as-is. An identifier built from user data (`user_4172@example.com_row`, an account number, an order ID) sends that data to Mixpanel. Before relying on click autocapture in production, turn on SDK logging, tap through your key flows, and confirm no `$el_id` contains PII.
</Warning>

#### Manual Click Tracking

`trackClick`, `trackRageClick` and `trackDeadClick` on the `autocapture` object send the same three events with element metadata you provide. Use them where automatic capture cannot reach: Windows and Linux, WebViews and platform views, or custom gesture handlers the hit test resolves to the wrong widget.

**Example Usage**

```dart theme={"system"}
// send "$mp_click" for a tap your app detected itself
await mixpanel.autocapture.trackClick(const ClickEvent(
  // required, tap position in logical pixels, sent as $x and $y
  x: 120,
  y: 340,
  // required, sent as $el_id
  elementId: 'checkout_button',
  // optional, sent as $el_tag_name, $attr-role and $elements
  tagName: 'ElevatedButton',
  role: 'Button',
  elements: 'ElevatedButton > Column',
));
```

* Each method accepts an optional `properties` map. The typed fields and `$mp_autocapture: true` take precedence over keys with the same name.
* A blank `elementId` or a non-finite coordinate is ignored and nothing is sent.
* Each call sends exactly one event and never starts the automatic detectors. For example, `trackClick` does not trigger dead click detection. Your app decides whether the signal happened.

#### Best Practices

* **Mount exactly one `MixpanelAutocaptureWidget`, above your navigators.** See [Enabling Click Autocapture](#enabling-click-autocapture).
* **Give every widget you want to measure a stable `Semantics(identifier: ...)`** on or above the widget that handles the tap. See [One-Time Setup: Element IDs](#one-time-setup-element-ids).
* **Keep identifiers static across rebuilds.** One built from a list index, timestamp or product name gets a new `$el_id` on each build and spreads one control across many rows.
* **Use `trackDeadClick` around WebViews, platform views and custom painting**, where [automatic detection is off](#dead-click).
* **Verify and check for PII before relying on the data.** Call `setLoggingEnabled(true)`, tap through your key flows, and confirm each `$el_id` is what you expect and [contains no personal data](#element-id-resolution).

## Debug Mode

To enable debug mode, call [`.setLoggingEnabled()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/setLoggingEnabled.html) with `true`.

**Example Usage**

```dart theme={"system"}
// enable debug logs
mixpanel.setLoggingEnabled(true);
```

Learn more about [debugging](/docs/tracking-best-practices/debugging).

## Privacy-Friendly Tracking

You have control over the data you send to Mixpanel. The Flutter SDK provide methods to help you protect user data.

Learn more about [Privacy](/docs/privacy).

### Opt Out of Tracking

The Android SDK is initialized with tracking enabled by default. Use the [`.optOutTracking()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/optOutTracking.html) method to opt the user out of data tracking and local storage for the current Mixpanel instance.

**Example Usage**

```dart theme={"system"}
//send "some_event"
mixpanel.track('some_event');

// opt user out of tracking
// SDK is prevented from sending any data
mixpanel.optOutTracking();

// this track call will not work
mixpanel.track('some_other_event');
```

**Opt Out by Default**

You can initialize the library with users opted out of tracking by default using the `optOutTrackingDefault` configuration. Once the user is ready to be tracked, call [`.optInTracking()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/optInTracking.html) to start tracking.

**Example Usage**

```dart theme={"system"}
...
// initialize Mixpanel with users opt out of tracking by default
mixpanel = await Mixpanel.init("YOUR_PROJECT_TOKEN", trackAutomaticEvents: false, optOutTrackingDefault: true);

// opt user back in to tracking afterwards
mixpanel.optInTracking();
...
```

### EU Data Residency

Route data to Mixpanel's EU servers by passing the `serverURL` parameter when initializing the client. This ensures every request is routed to the correct region from the very first event.

**Example Usage**

```dart theme={"system"}
// route data to Mixpanel's EU domain at initialization
mixpanel = await Mixpanel.init(
  "YOUR_PROJECT_TOKEN",
  trackAutomaticEvents: false,
  serverURL: "https://api-eu.mixpanel.com",
);
```

Alternatively, you can set the `serverURL` property after initializing the client.

**Example Usage**

```dart theme={"system"}
// set request URL to Mixpanel's EU domain
mixpanel.setServerURL("https://api-eu.mixpanel.com");
```

### India Data Residency

Route data to Mixpanel's India servers by passing the `serverURL` parameter when initializing the client. This ensures every request is routed to the correct region from the very first event.

**Example Usage**

```dart theme={"system"}
// route data to Mixpanel's India domain at initialization
mixpanel = await Mixpanel.init(
  "YOUR_PROJECT_TOKEN",
  trackAutomaticEvents: false,
  serverURL: "https://api-in.mixpanel.com",
);
```

Alternatively, you can set the `serverURL` property after initializing the client.

**Example Usage**

```dart theme={"system"}
// set request URL to Mixpanel's India domain
mixpanel.setServerURL("https://api-in.mixpanel.com");
```

### Disable Geolocation

The Flutter SDK parse the request IP address to generate geolocation properties for events and profiles. To disable geolocation, call [`.setUseIpAddressForGeolocation()`](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/setUseIpAddressForGeolocation.html) with `false`.

**Example Usage**

```dart theme={"system"}
// disable geolocation from IP parsing
mixpanel.setUseIpAddressForGeolocation(false);
```

Learn more about [geolocation](/docs/tracking-best-practices/geolocation).

## Legacy Automatically Tracked Events

Mixpanel's SDKs have a legacy feature to automatically collect common mobile events. We don't recommend enabling this, as these events rely on client-side state and can be unreliable compared to tracking server-side. You can still enable this feature by turning the flag `trackAutomaticEvents: true` when initializing Mixpanel. More details [here](https://github.com/mixpanel/mixpanel-flutter?tab=readme-ov-file#2-initialize-mixpanel).

You can see a list of events tracked automatically for [iOS here](/docs/tracking-methods/sdks/swift#legacy-automatically-tracked-events) and [Android here](/docs/tracking-methods/sdks/android#legacy-automatically-tracked-events).

## Tracking Via Proxy

This guide demonstrates how to route events from Mixpanel's Flutter SDKs via a proxy in your own domain. This is useful to reduce the likelihood of ad-blockers impacting your tracking.

There are two steps: setting up a proxy server and pointing the SDK at your server.

**Step 1: Set up a proxy server** The simplest way is to use our [sample nginx config](https://github.com/mixpanel/tracking-proxy). This config redirects any calls made to your proxy server to Mixpanel.

**Step 2: Point Flutter SDK at your server** Pass the `serverURL` parameter when initializing the client, replacing `YOUR_PROXY_DOMAIN` with your proxy server's domain. This ensures every request is routed to your proxy from the very first event.

**Example Usage**

```dart theme={"system"}
// route data to your proxy server at initialization
mixpanel = await Mixpanel.init(
  "YOUR_PROJECT_TOKEN",
  trackAutomaticEvents: false,
  serverURL: "https://<YOUR_PROXY_DOMAIN>",
);
```

Alternatively, you can set the `serverURL` property after initializing the client.

**Example Usage**

```dart theme={"system"}
// route data to your proxy server
mixpanel.setServerURL("https://<YOUR_PROXY_DOMAIN>");
```

Specifically for Flutter Web, add your proxy server to the `mixpanel.init` call:

**Example Usage**

```js theme={"system"}
mixpanel.init("<YOUR_PROJECT_TOKEN>", {api_host: "https://<YOUR_PROXY_DOMAIN>"});
```

## Release History

[See all releases.](https://github.com/mixpanel/mixpanel-flutter/releases)
