---
title: "Instrument and configure the Android-Kotlin SDK"
description: "Track events, manage identity and sessions, configure Autocapture, control data delivery, and extend the Amplitude Android-Kotlin SDK with plugins."
product: analytics
lang: en
last_updated: 2026-11-04
token_estimate: 9173
---
# Instrument and configure the Android-Kotlin SDK

This guide covers the everyday work with the Android-Kotlin SDK after [Get started](https://amplitude.com/docs/sdks/analytics/android/android-kotlin-sdk/get-started): tracking your own events, managing identity and sessions, choosing Autocapture features, controlling how events reach Amplitude, and extending the SDK with plugins. For exact signatures and every option, go to the [Reference](https://amplitude.com/docs/sdks/analytics/android/android-kotlin-sdk/reference). To find the release that added or changed a feature, go to [Android-Kotlin SDK release notes](https://amplitude.com/docs/sdks/analytics/android/android-kotlin-sdk-release-notes).

> For AI agents: a documentation index is available at [/docs/llms.txt](/docs/llms.txt). Append `.md` to any page URL for markdown, or send `Accept: text/markdown`.

The samples use an instance named `amplitude`, created as in [Initialize the SDK](https://amplitude.com/docs/sdks/analytics/android/android-kotlin-sdk/get-started#initialize-the-sdk).

## Track events

### Track a custom event

Pass an event name, and optionally a map of event properties:

#### Kotlin

```kotlin
amplitude.track("Song Played", mapOf("title" to "Happy Birthday", "duration_s" to 42))
```

#### Java

```java
Map<String, Object> properties = new HashMap<>();
properties.put("title", "Happy Birthday");
properties.put("duration_s", 42);
amplitude.track("Song Played", properties);
```

Don't track push notification events from the device. The SDK uploads only while the app runs, so a notification event can reach Amplitude long after it happened. Send those events from your server instead.

#### Pass the event as an object

Build a `BaseEvent` to set fields that the name-and-properties form doesn't take, such as event-level `groups` or your own `insertId` for deduplication:

```kotlin
import com.amplitude.core.events.BaseEvent

val event = BaseEvent().apply {
    eventType = "Song Played"
    eventProperties = mutableMapOf("title" to "Happy Birthday")
    groups = mutableMapOf("orgId" to "15")
    insertId = "song-played-1234"
}
amplitude.track(event)
```

#### Override fields for one event

Pass `EventOptions` to set fields such as `userId`, `timestamp`, or `sessionId` for one event only:

```kotlin
import com.amplitude.core.events.EventOptions

amplitude.track(
    "Purchase Synced",
    mapOf("order_id" to "A-100"),
    EventOptions().apply { timestamp = orderTimeMillis },
)
```

#### Get the upload result for an event

Pass a callback with a `BaseEvent`. The SDK calls it when it finishes with the event: `status` is `200` when Amplitude accepted it, or the error status when the SDK dropped it.

#### Kotlin

```kotlin
amplitude.track(BaseEvent().apply { eventType = "Button Clicked" }) { event, status, message ->
    Log.d("MyApp", "${event.eventType}: $status $message")
}
```

#### Java

```java
BaseEvent event = new BaseEvent();
event.setEventType("Button Clicked");
amplitude.track(event, null, (e, status, message) -> {
    Log.d("MyApp", e.getEventType() + ": " + status + " " + message);
    return Unit.INSTANCE;
});
```

To get results for every event, set the `callback` configuration option instead. For the statuses, go to [Read the upload result](https://amplitude.com/docs/sdks/analytics/android/android-kotlin-sdk/verify-events#read-the-upload-result).

### Set user properties

Build an `Identify` with one or more operations and pass it to `identify()`:

#### Kotlin

```kotlin
import com.amplitude.core.events.Identify

val identify = Identify()
    .set("color", "green")
    .setOnce("initial_source", "organic")
    .add("login_count", 1)
    .append("visited_pages", "home")
    .unset("temporary_property")
amplitude.identify(identify)
```

#### Java

```java
Identify identify = new Identify()
    .set("color", "green")
    .setOnce("initial_source", "organic")
    .add("login_count", 1)
    .append("visited_pages", "home")
    .unset("temporary_property");
amplitude.identify(identify);
```

For every operation, go to [Identify](https://amplitude.com/docs/sdks/analytics/android/android-kotlin-sdk/reference#identify). Use each property once per `Identify`; the SDK ignores a second operation on the same property. `clearAll()` removes every user property permanently.

To reduce the number of events it sends, the SDK holds identify events that contain only `set` operations and merges them. It sends the merged event after `identifyBatchIntervalMillis` (30 seconds by default), or right before the next other event, whichever comes first.

User property changes appear on the user's profile right away. Charts apply them to events the user sends afterward, not to earlier events.

### Assign a user to a group

Groups let you analyze by an account or organization, for example counting distinct organizations that performed an event. `setGroup()` assigns the current user to a group and sets `groupType` as a user property, replacing any earlier value for that type.

#### Kotlin

```kotlin
amplitude.setGroup("orgId", "15")
amplitude.setGroup("sport", arrayOf("tennis", "soccer")) // more than one group
```

#### Java

```java
amplitude.setGroup("orgId", "15");
amplitude.setGroup("sport", new String[] {"tennis", "soccer"});
```

To put one event in a group without changing the user, set `groups` on a `BaseEvent`. Go to [Pass the event as an object](#pass-the-event-as-an-object).

### Set group properties

```kotlin
amplitude.groupIdentify("plan", "enterprise", Identify().set("seats", 50))
```

Group property changes apply to events sent afterward. Amplitude supports up to 5 group types per event and 10 groups in total.

### Track revenue

Create a `Revenue` with at least `price`, and pass it to `revenue()`. The SDK sends a `revenue_amount` event.

#### Kotlin

```kotlin
import com.amplitude.core.events.Revenue

val revenue = Revenue().apply {
    productId = "com.example.premium"
    price = 3.99
    quantity = 3
}
amplitude.revenue(revenue)
```

#### Java

```java
Revenue revenue = new Revenue();
revenue.setProductId("com.example.premium");
revenue.setPrice(3.99);
revenue.setQuantity(3);
amplitude.revenue(revenue);
```

For revenue verification, set `revenueType` and call `revenue.setReceipt(receipt, receiptSignature)`, which sets `receipt` and `receiptSig`. For every field, go to [Revenue](https://amplitude.com/docs/sdks/analytics/android/android-kotlin-sdk/reference#revenue-class).

## Identity and sessions

### How the SDK chooses identifiers on startup

#### Device ID

On startup, the SDK uses the first of these that applies:

1. The `deviceId` configuration option, if you set it.
2. The device ID it stored on an earlier run, unless that ID came from the App Set ID.
3. The advertising ID, if `useAdvertisingIdForDeviceId` is `true`, `newDeviceIdPerInstall` is `false`, and the user hasn't limited ad tracking.
4. The App Set ID with `S` appended, if `useAppSetIdForDeviceId` is `true`.
5. A random UUID with `R` appended.

Because the SDK stores the device ID with the app's data, a backup restored to a new device can bring the old device ID with it. Set the same user ID on every device a user signs in to, so Amplitude merges their devices into one user.

#### User ID

The SDK restores the user ID it stored on an earlier run. A `setUserId()` call made before setup finishes wins over the stored value.

### Set the user ID

Set the user ID when a user signs in. `null` clears it.

```kotlin
amplitude.setUserId("user@example.com")
val userId = amplitude.getUserId()
```

### Reset identity when a user signs out

`reset()` clears the user ID and picks a device ID again, so later events count as a new anonymous user:

```kotlin
amplitude.reset()
```

`reset()` doesn't start a new session. It skips only the stored device ID, so it keeps the same device ID when you set the `deviceId` configuration option, `useAdvertisingIdForDeviceId`, or `useAppSetIdForDeviceId`. Amplitude then links events before and after the reset through that device ID. To start a separate anonymous user, set a new device ID after the reset:

```kotlin
amplitude.reset()
amplitude.setDeviceId(UUID.randomUUID().toString())
```

### Set the device ID

Use a value that's unique per device, such as a UUID:

```kotlin
amplitude.setDeviceId(UUID.randomUUID().toString())
```

### Read and update identity together

`amplitude.identity` returns the user ID, device ID, and local user properties. `setIdentity()` replaces all three in one step, so plugins such as Experiment see one change instead of several. Read the device ID after setup finishes:

```kotlin
// Inside a coroutine
amplitude.isBuilt.await()
amplitude.setIdentity("user-123", amplitude.getDeviceId(), mapOf("plan" to "paid"))
```

If the properties change, the SDK sends one identify event that sets them. The local properties live only in memory for the current process. Removing a property from the map doesn't remove it in Amplitude. To remove it, send `Identify().unset()`. Passing `null` as the device ID clears it. Requires 1.36.0 or later.

### How sessions work

A session starts when the app comes to the foreground, or when you track an event while no session is active. The session continues while the app is in the foreground. When the app goes to the background, the session ends after `minTimeBetweenSessionsMillis` (5 minutes by default) passes without an event. The next return to the foreground, or the next event, then starts a new session. An event tracked in the background within that time extends the session.

Events in a session share a `session_id`, which is the session's start time in milliseconds. With `SESSIONS` autocapture on (the default), the SDK also tracks `session_start` and `session_end` events. With it off, the SDK still assigns session IDs.

From 1.34.0, the SDK detects foreground and background from the process lifecycle, so multi-activity apps and apps that start a service before any activity get correct sessions.

Some session data can look incomplete:

- If the user never returns to the app, the SDK sends no `session_end` for that session.
- Events tracked in the background make a session look longer than the time the user spent in the app.
- A `session_end` sent after a user property change carries the new property values. To stop Amplitude from applying the event's user properties, set [`$skip_user_properties_sync`](https://amplitude.com/docs/data/converter-configuration-reference#skipuserpropertiessync) on it with an enrichment plugin.

#### Change the session timeout

```kotlin
val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    minTimeBetweenSessionsMillis = 10_000
}
```

#### Stop sending session events

Set `autocapture` without `SESSIONS`. Events still get session IDs.

```kotlin
val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    autocapture = emptySet()
}
```

#### Track an event outside a session

Set `sessionId = -1` in `EventOptions`. The event doesn't belong to, extend, or start a session, and doesn't change the session ID of later events. Use it for events the user didn't trigger in the app, such as a background sync.

```kotlin
val outOfSession = EventOptions().apply { sessionId = -1 }
amplitude.track(BaseEvent().apply { eventType = "Sync Completed" }, outOfSession)
amplitude.identify(Identify().set("synced", true), outOfSession)
```

### Opt users out of tracking

While `optOut` is `true`, the SDK drops every event and doesn't store it.

#### Kotlin

```kotlin
amplitude.optOut = true  // Stop tracking
amplitude.optOut = false // Resume tracking
```

#### Java

```java
amplitude.setOptOut(true);
amplitude.setOptOut(false);
```

To start opted out, set `optOut = true` in the configuration.

### Control which properties the SDK collects

Create a `TrackingOptions`, turn off the fields you don't want, and set it in the configuration:

#### Kotlin

```kotlin
import com.amplitude.android.TrackingOptions

val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    trackingOptions = TrackingOptions().disableIpAddress().disableLatLng().disableCarrier()
}
```

#### Java

```java
ConfigurationBuilder builder = new ConfigurationBuilder("YOUR_API_KEY", getApplicationContext());
builder.setTrackingOptions(new TrackingOptions().disableIpAddress().disableLatLng().disableCarrier());
Amplitude amplitude = new Amplitude(builder.build());
```

Amplitude derives city, region, and DMA from the IP address, so `disableIpAddress()` is what stops them. `disableCity()`, `disableRegion()`, `disableDma()`, and `disableApiLevel()` change nothing on the device. Turning a field off affects new events only; it doesn't remove data Amplitude already has. For every method, go to [TrackingOptions](https://amplitude.com/docs/sdks/analytics/android/android-kotlin-sdk/reference#trackingoptions).

#### Turn on COPPA control

For apps that must comply with the Children's Online Privacy Protection Act (COPPA), set `enableCoppaControl = true`. COPPA control stops the SDK from collecting the advertising ID, IP address, and latitude and longitude. Without the IP address, Amplitude also stops deriving city. COPPA control doesn't stop the App Set ID. To stop the App Set ID too, also call `disableAppSetId()`.

### Use the advertising ID or app set ID

The SDK can use a Google Play identifier as the device ID. Users can reset or turn off the advertising ID, and the advertising ID can be `null`. For most apps, the App Set ID is the more stable choice.

#### Advertising ID

1. Add the Play services dependency:

   ```kotlin
   implementation("com.google.android.gms:play-services-ads-identifier:18.0.1")
   ```
2. If your app targets Android 13 or later, declare the permission in `AndroidManifest.xml`. The SDK doesn't declare it for you.

   ```xml
   <uses-permission android:name="com.google.android.gms.permission.AD_ID" />
   ```
3. If you minify with R8 or ProGuard, keep the classes the SDK loads by reflection:

   ```text
   -keep class com.google.android.gms.ads.** { *; }
   -keep class com.google.android.gms.common.** { *; }
   ```
4. Turn on the advertising ID:

   ```kotlin
   val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
       useAdvertisingIdForDeviceId = true
   }
   ```

#### App Set ID

The App Set ID identifies an app install on a device without being tied to advertising. It resets after 13 months without use.

1. Add the dependency. The SDK ships keep rules for it.

   ```kotlin
   implementation("com.google.android.gms:play-services-appset:16.0.2")
   ```
2. Turn on the App Set ID:

   ```kotlin
   val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
       useAppSetIdForDeviceId = true
   }
   ```

### Track location

By default, Amplitude derives location from the IP address. To add the device's last known latitude and longitude, set `locationListening = true`, and request `ACCESS_COARSE_LOCATION` or `ACCESS_FINE_LOCATION` from the user. The SDK adds coordinates only while the app has the permission.

```kotlin
val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    locationListening = true
}
```

The default is `false`. [Default changed in 1.20.7](https://amplitude.com/docs/sdks/analytics/android/android-kotlin-sdk-release-notes#1207).

## Autocapture

### Choose autocapture features

By default, the SDK tracks session events only. To turn on more features, set `autocapture`. Requires 1.17.0 or later.

#### Kotlin

```kotlin
import com.amplitude.android.autocaptureOptions

val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    autocapture = autocaptureOptions {
        +sessions
        +appLifecycles
        +screenViews
        +deepLinks
        +elementInteractions
        +frustrationInteractions
    }
}
```

#### Java

```java
import com.amplitude.android.AutocaptureOption;

ConfigurationBuilder builder = new ConfigurationBuilder("YOUR_API_KEY", getApplicationContext());
builder.getAutocapture().add(AutocaptureOption.APP_LIFECYCLES);
builder.getAutocapture().add(AutocaptureOption.SCREEN_VIEWS);
builder.getAutocapture().add(AutocaptureOption.DEEP_LINKS);
builder.getAutocapture().add(AutocaptureOption.ELEMENT_INTERACTIONS);
builder.getAutocapture().add(AutocaptureOption.FRUSTRATION_INTERACTIONS);
Amplitude amplitude = new Amplitude(builder.build());
```

Setting `autocapture` replaces the default set, so include `+sessions` to keep session events. In Java, `getAutocapture()` already contains `SESSIONS`; call `remove(AutocaptureOption.SESSIONS)` to drop it. `AutocaptureOption.ALL` turns on everything.

With `enableAutocaptureRemoteConfig` on (the default), autocapture settings saved in Amplitude override the local set. Settings that Amplitude doesn't send keep their local values.

### Features and events

| Option | Events |
| --- | --- |
| `SESSIONS` | `session_start`, `session_end`. Go to [How sessions work](#how-sessions-work). |
| `APP_LIFECYCLES` | `[Amplitude] Application Installed`, `Updated`, `Opened`, `Backgrounded`. Go to [App lifecycles](#app-lifecycles). |
| `SCREEN_VIEWS` | `[Amplitude] Screen Viewed`, `[Amplitude] Fragment Viewed`. Go to [Screen views](#screen-views). |
| `DEEP_LINKS` | `[Amplitude] Deep Link Opened`. Go to [Deep links](#deep-links). |
| `ELEMENT_INTERACTIONS` | `[Amplitude] Element Interacted`. Go to [Element interactions](#element-interactions). |
| `FRUSTRATION_INTERACTIONS` | `[Amplitude] Rage Click`, `[Amplitude] Dead Click`. Go to [Frustration interactions](#frustration-interactions). |

### App lifecycles

| Event | When | Properties |
| --- | --- | --- |
| `[Amplitude] Application Installed` | The first launch after install. | `[Amplitude] Version`, `[Amplitude] Build` |
| `[Amplitude] Application Updated` | The first launch after the build number (`versionCode`) changes. | `[Amplitude] Version`, `[Amplitude] Build`, `[Amplitude] Previous Version`, `[Amplitude] Previous Build` |
| `[Amplitude] Application Opened` | Each launch or return to the foreground after the first. | `[Amplitude] Version`, `[Amplitude] Build`, `[Amplitude] From Background` |
| `[Amplitude] Application Backgrounded` | The app goes to the background. | None |

### Screen views

The SDK tracks `[Amplitude] Screen Viewed` for each activity and `[Amplitude] Fragment Viewed` for each fragment. Fragment tracking needs `androidx.fragment` in your app and requires 1.18.0 or later.

| Property | Description |
| --- | --- |
| `[Amplitude] Screen Name` | The activity's title, then its label, then its name, then its class name, whichever is set first. |
| `[Amplitude] Fragment Class` | Fragment Viewed only. The fragment's fully qualified class name. |
| `[Amplitude] Fragment Identifier` | Fragment Viewed only. The resource name of the fragment's ID or container view. |
| `[Amplitude] Fragment Tag` | Fragment Viewed only. The fragment's tag. |

### Deep links

The SDK tracks `[Amplitude] Deep Link Opened` with `[Amplitude] Link URL` and `[Amplitude] Link Referrer` when an activity starts or resumes with an intent that has data.

If the activity uses the `singleTop`, `singleTask`, or `singleInstance` launch mode, Android delivers a new link to `onNewIntent()` without restarting the activity. Call `setIntent()` there, or the SDK keeps reading the original intent:

#### Kotlin

```kotlin
override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    setIntent(intent)
}
```

#### Java

```java
@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    setIntent(intent);
}
```

### Element interactions

The SDK tracks `[Amplitude] Element Interacted` when a user interacts with an element, in both Android Views and Jetpack Compose. From 1.35.0, it also tracks non-tap gestures such as swipes and long presses, and selectable Compose controls such as `Checkbox` and `Switch`.

| Property | Description |
| --- | --- |
| `[Amplitude] Action` | `touch`, `valueChange`, `pan`, `longPress`, `pinch`, or `rotation`. |
| `[Amplitude] Target Class` | The view's class name. Views only. |
| `[Amplitude] Target Resource` | The resource entry name of the view's ID. Views only. |
| `[Amplitude] Target Tag` | The view's tag when it's a primitive value, or the Compose `Modifier.testTag`. |
| `[Amplitude] Target Text` | The text of a `Button`. Views only. |
| `[Amplitude] Target Accessibility Label` | The element's accessibility label. Requires 1.23.0 or later. |
| `[Amplitude] Target Source` | `Android View` or `Jetpack Compose`. |
| `[Amplitude] Hierarchy` | The chain of parent views. Views only. |
| `[Amplitude] Screen Name` | Go to [Screen views](#screen-views). |

Compose elements have no class, resource, text, or hierarchy, so add `Modifier.testTag` to the composables you want to tell apart. A tag also stays stable when you restyle the UI:

```kotlin
Button(onClick = { /* ... */ }, modifier = Modifier.testTag("login_button")) {
    Text("Log In")
}
```

### Frustration interactions

`FRUSTRATION_INTERACTIONS` tracks two events. Requires 1.22.0 or later.

- **`[Amplitude] Rage Click`**: four or more clicks on the same element within one second, no more than 50 dp apart. The event reports every click in the burst.
- **`[Amplitude] Dead Click`**: a click on an interactive element followed by no visible change within three seconds. Dead click detection needs a plugin that reports UI changes, which the [Session Replay Android plugin](https://amplitude.com/docs/sdks/session-replay/session-replay-android-plugin) provides. Without one, the SDK tracks no dead clicks.

Both events carry the [Element interactions](#element-interactions) properties, plus:

| Property | Description |
| --- | --- |
| `[Amplitude] Begin Time` | When the first click happened, as an ISO-8601 UTC timestamp. |
| `[Amplitude] End Time` | Rage click: when the last click happened. Dead click: three seconds after the click. ISO-8601 UTC. |
| `[Amplitude] Duration` | Milliseconds between Begin Time and End Time. Always `3000` for a dead click. |
| `[Amplitude] X`, `[Amplitude] Y` | The first click's position, from the top-left of the screen. |
| `[Amplitude] Click Count` | The number of clicks. Always `1` for a dead click. |
| `[Amplitude] Clicks` | Rage click only. A list of clicks, each with `[Amplitude] X`, `[Amplitude] Y`, and `Time`. |

To track only one of the two, set `interactionsOptions`:

```kotlin
import com.amplitude.android.DeadClickOptions
import com.amplitude.android.InteractionsOptions
import com.amplitude.android.RageClickOptions

val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    autocapture = autocaptureOptions { +frustrationInteractions }
    interactionsOptions = InteractionsOptions(
        rageClick = RageClickOptions(enabled = true),
        deadClick = DeadClickOptions(enabled = false),
    )
}
```

#### Exclude elements from frustration tracking

Users tap some elements quickly by design, such as stepper buttons. Other elements never change the screen, such as a close button. Exclude these elements from rage or dead click detection. The SDK still tracks `[Amplitude] Element Interacted` for them.

#### Kotlin

```kotlin
import com.amplitude.android.FrustrationAnalyticsUtils
import com.amplitude.android.ignoreFrustrationAnalytics

// Views: ignore both, or only rage clicks
FrustrationAnalyticsUtils.ignoreFrustrationAnalytics(backButton)
FrustrationAnalyticsUtils.ignoreFrustrationAnalytics(incrementButton, rageClick = true, deadClick = false)
FrustrationAnalyticsUtils.unignoreView(backButton)

// Compose: ignore only dead clicks
Button(
    onClick = { submitForm() },
    modifier = Modifier.ignoreFrustrationAnalytics(rageClick = false, deadClick = true),
) { Text("Submit") }
```

#### Java

```java
FrustrationAnalyticsUtils.INSTANCE.ignoreFrustrationAnalytics(backButton, true, true);
FrustrationAnalyticsUtils.INSTANCE.ignoreFrustrationAnalytics(incrementButton, true, false);
FrustrationAnalyticsUtils.INSTANCE.unignoreView(backButton);
```

`rageClick = true` ignores rage clicks and `deadClick = true` ignores dead clicks. Both default to `true`.

### Network tracking

`NetworkTrackingPlugin` tracks `[Amplitude] Network Request` events for HTTP requests made through OkHttp. It isn't part of `autocapture`: add it as an interceptor on each OkHttp client you want to track, and add it to the instance. Requires 1.21.0 or later, and OkHttp in your app.

```kotlin
import com.amplitude.android.network.NetworkTrackingPlugin

val networkPlugin = NetworkTrackingPlugin()
val okHttpClient = OkHttpClient.Builder()
    .addInterceptor(networkPlugin)
    .build()
amplitude.add(networkPlugin)
```

By default, the plugin captures responses with status 500 to 599 from every host except Amplitude. To choose hosts and statuses, pass `NetworkTrackingOptions`:

```kotlin
import com.amplitude.android.network.NetworkTrackingOptions
import com.amplitude.android.network.NetworkTrackingOptions.CaptureRule

val options = NetworkTrackingOptions(
    captureRules = listOf(
        CaptureRule(hosts = listOf("*"), statusCodeRange = (400..599).toList()),
        CaptureRule(hosts = listOf("*.example.com", "example.com"), statusCodeRange = (500..599).toList()),
    ),
    ignoreHosts = listOf("analytics.example.com"),
)
amplitude.add(NetworkTrackingPlugin(options))
```

The plugin first skips `ignoreHosts` and, by default, Amplitude's own requests. It then checks rules from the last to the first, and the first rule whose hosts match decides. With the rules above, the plugin captures a `503` from `example.com`. It doesn't capture a `401` from `example.com`, because the last rule matched the host but not the status. It captures a `401` from `other.com` through the first rule.

To match by URL, filter by method, or capture headers and JSON body fields, use the URL-based rule. Requires 1.28.0 or later.

```kotlin
import com.amplitude.android.network.NetworkTrackingOptions.CaptureBody
import com.amplitude.android.network.NetworkTrackingOptions.CaptureHeader
import com.amplitude.android.network.NetworkTrackingOptions.URLPattern

CaptureRule(
    urls = listOf(URLPattern.Regex("^https://api\\.example\\.com/v1/.*")),
    methods = listOf("POST"),
    statusCodeRange = (200..599).toList(),
    requestHeaders = CaptureHeader(allowlist = listOf("X-Request-Id")),
    responseHeaders = CaptureHeader(allowlist = listOf("X-Response-Id")),
    requestBody = CaptureBody(allowlist = listOf("user/*"), excludelist = listOf("**/password")),
    responseBody = CaptureBody(allowlist = listOf("profile/**")),
)
```

The plugin never captures the `authorization`, `cookie`, or `proxy-authorization` headers, and it masks credentials and common sensitive query parameters in URLs. For every option, go to [NetworkTrackingPlugin](https://amplitude.com/docs/sdks/analytics/android/android-kotlin-sdk/reference#networktrackingplugin).

| Property | Description |
| --- | --- |
| `[Amplitude] URL`, `[Amplitude] URL Query`, `[Amplitude] URL Fragment` | The request URL, with sensitive parts masked, and its parts. |
| `[Amplitude] Request Method`, `[Amplitude] Status Code` | The HTTP method and response status. |
| `[Amplitude] Error Message` | The local error when the request failed without a status. |
| `[Amplitude] Start Time`, `[Amplitude] Completion Time`, `[Amplitude] Duration` | Timing, in milliseconds. |
| `[Amplitude] Request Body Size`, `[Amplitude] Response Body Size` | Body sizes in bytes. |
| `[Amplitude] Request Headers`, `[Amplitude] Response Headers`, `[Amplitude] Request Body`, `[Amplitude] Response Body` | Present when a rule captures them. |

### Track media playback

To track Media3 and ExoPlayer playback, add `com.amplitude:streaming-analytics-android` at the same version as `analytics-android`. The SDK installs the package automatically when the package is on the classpath. Requires 1.33.0 or later. For changes to media playback events, go to the [release notes](https://amplitude.com/docs/sdks/analytics/android/android-kotlin-sdk-release-notes).

## Data delivery

### How the SDK uploads events

The SDK writes each event to a file in the app's storage, then uploads queued events when:

- 30 events (`flushQueueSize`) are waiting.
- 30 seconds (`flushIntervalMillis`) pass after an event is queued.
- The app goes to the background (`flushEventsOnClose`, on by default).
- The device reconnects after being offline.
- You call `amplitude.flush()`.

Stored events survive app restarts. When an upload fails, the SDK keeps the events and retries with growing delays: about 1, 8, 32, and 128 seconds, then every 5 minutes, with no limit. After a `429`, it waits 30 seconds. It drops events only when Amplitude rejects them, for example for an invalid API key. The deprecated `flushMaxRetries` option has no effect. Retries became unlimited in 1.31.0.

### Tune upload performance

Raise `flushIntervalMillis` and `flushQueueSize` to upload less often, or lower them to see events sooner:

#### Kotlin

```kotlin
val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    flushIntervalMillis = 60_000
    flushQueueSize = 50
}
```

#### Java

```java
ConfigurationBuilder builder = new ConfigurationBuilder("YOUR_API_KEY", getApplicationContext());
builder.setFlushIntervalMillis(60000);
builder.setFlushQueueSize(50);
Amplitude amplitude = new Amplitude(builder.build());
```

`flushQueueSize` sets when an upload starts, not how many events a request carries. To upload large volumes through the Batch API, set `useBatch = true`.

### Choose a data region

For a project in Amplitude's EU data center, set `serverZone`. Use the API key from the EU project.

#### Kotlin

```kotlin
import com.amplitude.core.ServerZone

val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    serverZone = ServerZone.EU
}
```

#### Java

```java
ConfigurationBuilder builder = new ConfigurationBuilder("YOUR_API_KEY", getApplicationContext());
builder.setServerZone(ServerZone.EU);
Amplitude amplitude = new Amplitude(builder.build());
```

### Send events through a proxy

Set `serverUrl` to your proxy's URL. The proxy forwards the request to Amplitude's endpoint for your data center and returns Amplitude's response unchanged, so the SDK can read upload results.

```kotlin
val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    serverUrl = "https://analytics.example.com/2/httpapi"
}
```

The SDK sends plain JSON to a custom `serverUrl`. To compress the request body, set `enableRequestBodyCompression = true`; your proxy must accept `Content-Encoding: gzip`.

`serverUrl` covers event uploads only. The SDK still fetches remote configuration from `sr-client-cfg.amplitude.com` (EU: `sr-client-cfg.eu.amplitude.com`) and sends its diagnostics to Amplitude directly.

### Work offline

The SDK holds events while the device has no network and uploads them when the device reconnects. Offline mode needs the `ACCESS_NETWORK_STATE` permission. Without it, the SDK logs `No ACCESS_NETWORK_STATE permission, offline mode is not supported. ...` and treats the device as always online, so uploads fail and retry instead. Requires 1.13.0 or later.

To control offline mode yourself, for example to hold uploads until the user gives consent, set `offline = AndroidNetworkConnectivityCheckerPlugin.Disabled` in the configuration. This setting removes the connectivity check. Then set `amplitude.configuration.offline` to `true` or `false` from your code.

### Use a custom HTTP client

The SDK uploads with `HttpURLConnection`. To use another client, such as OkHttp for certificate pinning or custom timeouts, implement `HttpClientInterface` and set `httpClient`. Requires 1.20.0 or later. The SDK repository has a [sample OkHttp client](https://github.com/amplitude/Amplitude-Kotlin/blob/main/samples/kotlin-android-app/src/main/java/com/amplitude/android/sample/CustomOkHttpClient.kt) with a [gzip interceptor](https://github.com/amplitude/Amplitude-Kotlin/blob/main/samples/kotlin-android-app/src/main/java/com/amplitude/android/sample/GzipRequestInterceptor.kt).

#### Kotlin

```kotlin
val httpClient = CustomOkHttpClient()
val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    this.httpClient = httpClient
}
httpClient.initialize(amplitude.configuration)
```

#### Java

```java
CustomOkHttpClient httpClient = new CustomOkHttpClient();
ConfigurationBuilder builder = new ConfigurationBuilder("YOUR_API_KEY", getApplicationContext());
builder.setHttpClient(httpClient);
Configuration configuration = builder.build();
httpClient.initialize(configuration);
Amplitude amplitude = new Amplitude(configuration);
```

A custom client handles compression itself.

## Plugins

Plugins run code on every event or react to identity changes. The SDK runs `Before`, then `Enrichment`, then `Destination` plugins for each event. A `Before` or `Enrichment` plugin returns the event, changed or not, or `null` to drop it. For every hook, go to [Plugin](https://amplitude.com/docs/sdks/analytics/android/android-kotlin-sdk/reference#plugin).

### Add properties to every event

#### Kotlin

```kotlin
import com.amplitude.core.Amplitude
import com.amplitude.core.events.BaseEvent
import com.amplitude.core.platform.Plugin

class AppFlavorPlugin : Plugin {
    override val type = Plugin.Type.Enrichment
    override lateinit var amplitude: Amplitude

    override fun execute(event: BaseEvent): BaseEvent {
        event.eventProperties = (event.eventProperties ?: mutableMapOf()).apply {
            put("app_flavor", BuildConfig.FLAVOR)
        }
        return event
    }
}

amplitude.add(AppFlavorPlugin())
```

#### Java

```java
public class AppFlavorPlugin implements Plugin {
    private Amplitude amplitude;

    @NonNull @Override public Type getType() { return Type.Enrichment; }
    @NonNull @Override public Amplitude getAmplitude() { return amplitude; }
    @Override public void setAmplitude(@NonNull Amplitude amplitude) { this.amplitude = amplitude; }
    @Override public void setup(@NonNull Amplitude amplitude) { this.amplitude = amplitude; }

    @Override
    public BaseEvent execute(@NonNull BaseEvent event) {
        if (event.getEventProperties() == null) {
            event.setEventProperties(new HashMap<>());
        }
        event.getEventProperties().put("app_flavor", BuildConfig.FLAVOR);
        return event;
    }
}

amplitude.add(new AppFlavorPlugin());
```

To run before every other enrichment, for example to guard a field other plugins read, use `Plugin.Type.Before`.

### Send events to another destination

Extend `DestinationPlugin` and override `track()`. Override `identify()`, `groupIdentify()`, `revenue()`, and `flush()` as needed. Don't override `execute()`; it's final on destination plugins.

```kotlin
import com.amplitude.core.events.BaseEvent
import com.amplitude.core.platform.DestinationPlugin

class LogcatDestination : DestinationPlugin() {
    override fun track(payload: BaseEvent): BaseEvent {
        Log.d("Events", "${payload.eventType} ${payload.eventProperties}")
        return payload
    }
}

amplitude.add(LogcatDestination())
```

### React to identity changes

Extend `ObservePlugin` to react to sign-in, sign-out, and other identity changes without touching events. Override `onUserIdChanged` and `onDeviceIdChanged`, plus any of `onSessionIdChanged`, `onOptOutChanged`, `onIdentityChanged`, and `onReset`.

```kotlin
import com.amplitude.core.Amplitude
import com.amplitude.core.platform.ObservePlugin

class LoginObserver : ObservePlugin() {
    override lateinit var amplitude: Amplitude

    override fun onUserIdChanged(userId: String?) {
        // React to sign-in or sign-out.
    }

    override fun onDeviceIdChanged(deviceId: String?) {}
}

amplitude.add(LoginObserver())
```

### Remove a plugin

Pass the same instance you added:

```kotlin
val plugin = AppFlavorPlugin()
amplitude.add(plugin)
amplitude.remove(plugin)
```

## Run more than one instance

To send events to more than one project, create one instance per project, each with its own `instanceName`. Each instance keeps its own storage, identity, and queue.

```kotlin
val product = Amplitude("PRODUCT_API_KEY", applicationContext) {
    instanceName = "product"
}
val marketing = Amplitude("MARKETING_API_KEY", applicationContext) {
    instanceName = "marketing"
}
```

Create each instance once, for example in `Application.onCreate`, and inject it where you need it. With Hilt, Koin, or Dagger, provide it as an application-scoped singleton. Creating a second instance with the same `instanceName` retires the first one, which then drops new events with `Dropping event '<type>': this Amplitude instance was replaced.` Requires 1.30.1 or later.

