How to add product analytics to an Android app with Kotlin

Track report opens and date-range changes with Amplitude's Android Kotlin SDK. Allow about 25 minutes after Android Studio and the native SDK tools are installed.

  • Android API 23+
  • Kotlin
  • Android Kotlin SDK 1.x
  • 25 min

Chris Tam · Last updated 26 October 2026

Android apps keep analytics outside individual screens so navigation and activity recreation can share one client. Amplitude product analytics connects those screen actions to the reports your team uses to decide what to improve.

In this tutorial, you'll build a small usage-report screen, track its two actions, and connect an existing sign-in flow through explicit account callbacks.

A native Android report screen that sends Report Previewed and Report Range Changed with report_type, days, and platform. You'll use its device ID to find the events in Amplitude and verify signed-in behavior when you connect your own authentication.

Prerequisites

  • Android Studio with Android SDK platform 36, Build Tools 36.0.0, and an Android API 23+ emulator or device. The sample uses Android Views, AGP 9.2.0, Gradle 9.4.1, and JDK 17.
  • An Amplitude project API key from Organization settings > API Keys; select your project. Use the region that matches that key.
  • For signed-in verification, an existing authentication implementation and canonical user IDs of at least five characters.

No Amplitude account yet? A free account is enough for the explicit events in this tutorial. Sign up for free to get your project API key.

1. Create the Android project

I create an Android Studio project named AmplitudeReports with the No Activity template, Kotlin, Kotlin DSL, package com.example.amplitudereports, and minimum SDK 23. Keep the generated gradlew, gradlew.bat, gradle-wrapper.jar, and machine-specific local.properties. The custom files below supply the entire application; no sibling Build tutorial is required.

For an existing app, add the SDK dependency and merge the application, manifest, and event calls into its current structure. Keep its plugin versions, build variants, permissions, routes, authentication, and other instrumentation. The complete Gradle files below describe the fresh sample, not replacements for an existing app's build.

The mobile installation guide supplies the SDK dependency. I pin version 1.35.0 instead of a dynamic 1.+ range so the example names the artifact it uses.

settings.gradle.kts

settings.gradle.kts
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}
rootProject.name = "AmplitudeReports"
include(":app")

build.gradle.kts

build.gradle.kts
plugins {
    id("com.android.application") version "9.2.0" apply false
}

gradle.properties

gradle.properties
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true

gradle/wrapper/gradle-wrapper.properties

gradle/wrapper/gradle-wrapper.properties
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

app/build.gradle.kts

app/build.gradle.kts
plugins {
    id("com.android.application")
}
android {
    namespace = "com.example.amplitudereports"
    compileSdk = 36
    defaultConfig {
        applicationId = "com.example.amplitudereports"
        minSdk = 23
        targetSdk = 36
        versionCode = 1
        versionName = "1.0"
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}
dependencies {
    implementation("com.amplitude:analytics-android:1.35.0")
}

2. Initialize in the application

I register one Application subclass in the manifest and construct the client there, following the SDK initialization guidance. Setting autocapture to an empty set keeps this sample on explicit instrumentation; disabling its remote configuration keeps that choice local.

app/src/main/AndroidManifest.xml

app/src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:name=".ReportsApplication"
        android:allowBackup="false"
        android:label="Amplitude Reports"
        android:supportsRtl="true"
        android:theme="@android:style/Theme.Material.Light.NoActionBar">
        <activity android:name=".MainActivity" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

app/src/main/java/com/example/amplitudereports/AmplitudeConfig.kt

app/src/main/java/com/example/amplitudereports/AmplitudeConfig.kt
package com.example.amplitudereports

import com.amplitude.core.ServerZone

object AmplitudeConfig {
    const val API_KEY = ""
    val SERVER_ZONE = ServerZone.US
}

app/src/main/java/com/example/amplitudereports/ReportsApplication.kt

app/src/main/java/com/example/amplitudereports/ReportsApplication.kt
package com.example.amplitudereports

import android.app.Application

class ReportsApplication : Application() {
    lateinit var analytics: ReportAnalytics
        private set

    override fun onCreate() {
        super.onCreate()
        analytics = ReportAnalytics(this)
    }
}

Set API_KEY to your project's API key and choose ServerZone.EU for an EU project. A mobile project key ships in the application; keep management secrets on your server. If your app already initializes analytics, reuse its existing instance instead of constructing another one.

3. Connect your existing account lifecycle

I keep the two report events and account callbacks in one adapter. On verified sign-in, call accountSignedIn before tracking the account's actions. Initial anonymous-to-known sign-in sets the user ID without resetting the device. On a confirmed account switch, call accountSignedOut for the previous account before accountSignedIn for the next one.

app/src/main/java/com/example/amplitudereports/ReportAnalytics.kt

app/src/main/java/com/example/amplitudereports/ReportAnalytics.kt
package com.example.amplitudereports

import android.app.Application
import com.amplitude.android.Amplitude
import com.amplitude.core.events.Identify

class ReportAnalytics(application: Application) {
    // Reuse your app's existing instance instead if analytics is already initialized.
    private val amplitude: Amplitude? = AmplitudeConfig.API_KEY
        .takeIf { it.isNotBlank() }
        ?.let { key ->
            Amplitude(key, application.applicationContext) {
                serverZone = AmplitudeConfig.SERVER_ZONE
                autocapture = emptySet()
                enableAutocaptureRemoteConfig = false
            }
        }

    val configured: Boolean get() = amplitude != null
    fun deviceId(): String? = amplitude?.getDeviceId()

    fun reportPreviewed(days: Int) {
        amplitude?.track("Report Previewed", reportProperties(days))
    }

    fun reportRangeChanged(days: Int) {
        amplitude?.track("Report Range Changed", reportProperties(days))
    }

    // Call after your existing authentication confirms the account, before its events.
    fun accountSignedIn(canonicalUserId: String, plan: String) {
        require(canonicalUserId.length >= 5) { "Use a canonical ID of at least five characters" }
        amplitude?.setUserId(canonicalUserId)
        amplitude?.identify(Identify().set("plan", plan))
    }

    // Call only after confirmed logout, never for a loading or anonymous session.
    fun accountSignedOut() {
        amplitude?.reset()
    }

    fun flush() {
        amplitude?.flush()
    }

    private fun reportProperties(days: Int): Map<String, Any> = mapOf(
        "report_type" to "usage",
        "days" to days,
        "platform" to "Android",
    )
}

Call accountSignedOut only after confirmed logout. A null or loading auth state is not a logout event. Wait until your existing authentication is resolved before enabling tracked actions in a signed-in app. The clean sample has no login screen; account callbacks are integration hooks, not an authentication system.

Why. Activity recreation should not reset identity or initialize a second client. Application owns the client, while your verified auth callbacks own account transitions. The SDK handles its usual event buffering; flush requests an upload and does not guarantee delivery before the OS stops the app.

4. Track the report actions

I build a small screen with Android platform views. Open report tracks the current range; Change date range switches between seven and 30 days. The counts on screen are local sample data. Saving screen state avoids losing the selected range on recreation without sending an extra report event just because onCreate runs.

app/src/main/java/com/example/amplitudereports/MainActivity.kt

app/src/main/java/com/example/amplitudereports/MainActivity.kt
package com.example.amplitudereports

import android.app.Activity
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowInsets
import android.widget.Button
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView

class MainActivity : Activity() {
    private val analytics: ReportAnalytics
        get() = (application as ReportsApplication).analytics
    private var days = 7
    private var opened = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        days = savedInstanceState?.getInt("days", 7) ?: 7
        opened = savedInstanceState?.getBoolean("opened", false) ?: false
        val padding = (24 * resources.displayMetrics.density).toInt()
        val column = LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            setPadding(padding, padding, padding, padding)
        }
        val scroll = ScrollView(this).apply { addView(column) }
        scroll.setOnApplyWindowInsetsListener { view: View, insets: WindowInsets ->
            if (Build.VERSION.SDK_INT >= 30) {
                val safe = insets.getInsets(
                    WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout(),
                )
                view.setPadding(safe.left, safe.top, safe.right, safe.bottom)
            } else if (Build.VERSION.SDK_INT >= 28) {
                val cutout = insets.displayCutout
                view.setPadding(
                    maxOf(insets.systemWindowInsetLeft, cutout?.safeInsetLeft ?: 0),
                    maxOf(insets.systemWindowInsetTop, cutout?.safeInsetTop ?: 0),
                    maxOf(insets.systemWindowInsetRight, cutout?.safeInsetRight ?: 0),
                    maxOf(insets.systemWindowInsetBottom, cutout?.safeInsetBottom ?: 0),
                )
            } else {
                view.setPadding(
                    insets.systemWindowInsetLeft, insets.systemWindowInsetTop,
                    insets.systemWindowInsetRight, insets.systemWindowInsetBottom,
                )
            }
            insets
        }
        setContentView(scroll)
        scroll.requestApplyInsets()
        column.addView(TextView(this).apply {
            text = "Usage reports"
            textSize = 28f
        })
        val status = TextView(this).apply {
            text = if (analytics.configured) "Analytics configured; verify delivery separately"
                else "Analytics off: set the project API key"
        }
        column.addView(status)
        val report = TextView(this)
        fun render() {
            report.text = if (opened) "$days-day report: ${days * 12} sample actions"
                else "Open the sample report"
        }
        render()
        column.addView(report)
        column.addView(Button(this).apply {
            text = "Open report"
            setOnClickListener {
                opened = true
                render()
                analytics.reportPreviewed(days)
            }
        })
        column.addView(Button(this).apply {
            text = "Change date range"
            setOnClickListener {
                days = if (days == 7) 30 else 7
                opened = true
                render()
                analytics.reportRangeChanged(days)
            }
        })
        column.addView(Button(this).apply {
            text = "Flush and show device ID"
            setOnClickListener {
                analytics.flush()
                status.text = "Device ID: ${analytics.deviceId() ?: "not available yet"}; flush requested"
            }
        })
    }

    override fun onSaveInstanceState(outState: Bundle) {
        outState.putInt("days", days)
        outState.putBoolean("opened", opened)
        super.onSaveInstanceState(outState)
    }
}

The status says configured rather than delivered. The Flush and show device ID button requests a flush and displays the current ID for this development check; it is not an ingestion receipt. Remove that diagnostic control from your production screen.

5. Build and check the app

Sync Gradle, then build the debug app from its project directory. On Windows use gradlew.bat in place of ./gradlew.

Terminal

Terminal
./gradlew :app:assembleDebug
./gradlew :app:installDebug
adb shell am start -n com.example.amplitudereports/.MainActivity

Without a key, the sample screen still works and reports analytics off. With a key, tap the two actions, then request a flush. Rebuild after changing the Kotlin configuration file. A successful APK build establishes compilation; use the checks below to establish delivery and identity behavior.

Verify it worked

  1. Start a fresh test installation, open the report, then change its range. The sample switches to 30 days and shows 360 sample actions. Rotate the device and confirm the range survives without another Report Previewed event. Check that the controls remain clear of side system bars and display cutouts in landscape.
  2. Tap Flush and show device ID. If initialization has not supplied an ID yet, wait and tap again. Inspect Logcat for errors without logging private event contents or interpreting the local status as delivery proof.
  3. Open Data > Sources > Ingestion Debugger for the same project. Check request/event counts, then go to Users > User Profiles and search by the displayed device ID. In the event stream, inspect Report Previewed with days=7 and Report Range Changed with days=30, report_type=usage, and platform=Android. The debugger shows counts; the profile shows individual events.
  4. With your existing auth connected, sign in and call accountSignedIn with the verified canonical ID and plan. Track another report event, then find the signed-in profile and plan property. Confirm first login retained the device ID. Log out through the real app, call accountSignedOut, and confirm the next anonymous event uses a new device ID. Check account switching separately.
  5. Test offline/reconnect and background behavior on a device. A flush request or locally queued event does not establish lossless delivery during force quit or OS suspension.

Troubleshooting

The screen works but analytics is off. Set the project API key in AmplitudeConfig.kt and rebuild. An empty key intentionally skips SDK construction.

The client initializes more than once. Keep one instance in Application.onCreate. Register ReportsApplication in the manifest and reuse the existing client when merging this into another app.

Events are absent from the profile. Check the key and region pair, INTERNET permission, Logcat errors, and the current device ID. Request a flush; check counts and profiles separately.

Signed-in events have the previous account. Wait for verified authentication before enabling tracked actions. On confirmed account switching, reset the previous identity before setting the next canonical ID. Do not reset during session loading.

Gradle reports a Kotlin plugin conflict. This fresh sample uses AGP 9.2's built-in Kotlin. It does not apply org.jetbrains.kotlin.android. Existing apps should retain their supported plugin setup and merge the SDK changes instead of copying this sample's plugin block.

Frequently asked questions

Does this use the old Android SDK?

No. The dependency is com.amplitude:analytics-android, the Android Kotlin SDK documented for new applications. It is separate from the maintenance com.amplitude:android-sdk package.

Do I need Compose or a second Build tutorial?

No. The sample uses Android platform views, includes all custom application files, and starts with a fresh native project. Existing Compose apps can call the same analytics adapter from their action handlers.

Does this record sessions or install replay?

This sample disables analytics autocapture and adds explicit report events. Session replay has its own installation and configuration; adding the analytics dependency alone does not establish replay capture.

Full code reference

Keep the Android Studio-generated wrapper launchers/JAR and your local SDK path. These are all complete custom files for the sample. Preserve existing app files when merging the integration.

settings.gradle.kts

settings.gradle.kts
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}
rootProject.name = "AmplitudeReports"
include(":app")

build.gradle.kts

build.gradle.kts
plugins {
    id("com.android.application") version "9.2.0" apply false
}

gradle.properties

gradle.properties
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true

gradle/wrapper/gradle-wrapper.properties

gradle/wrapper/gradle-wrapper.properties
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

app/build.gradle.kts

app/build.gradle.kts
plugins {
    id("com.android.application")
}
android {
    namespace = "com.example.amplitudereports"
    compileSdk = 36
    defaultConfig {
        applicationId = "com.example.amplitudereports"
        minSdk = 23
        targetSdk = 36
        versionCode = 1
        versionName = "1.0"
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}
dependencies {
    implementation("com.amplitude:analytics-android:1.35.0")
}

app/src/main/AndroidManifest.xml

app/src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:name=".ReportsApplication"
        android:allowBackup="false"
        android:label="Amplitude Reports"
        android:supportsRtl="true"
        android:theme="@android:style/Theme.Material.Light.NoActionBar">
        <activity android:name=".MainActivity" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

app/src/main/java/com/example/amplitudereports/AmplitudeConfig.kt

app/src/main/java/com/example/amplitudereports/AmplitudeConfig.kt
package com.example.amplitudereports

import com.amplitude.core.ServerZone

object AmplitudeConfig {
    const val API_KEY = ""
    val SERVER_ZONE = ServerZone.US
}

app/src/main/java/com/example/amplitudereports/ReportsApplication.kt

app/src/main/java/com/example/amplitudereports/ReportsApplication.kt
package com.example.amplitudereports

import android.app.Application

class ReportsApplication : Application() {
    lateinit var analytics: ReportAnalytics
        private set

    override fun onCreate() {
        super.onCreate()
        analytics = ReportAnalytics(this)
    }
}

app/src/main/java/com/example/amplitudereports/ReportAnalytics.kt

app/src/main/java/com/example/amplitudereports/ReportAnalytics.kt
package com.example.amplitudereports

import android.app.Application
import com.amplitude.android.Amplitude
import com.amplitude.core.events.Identify

class ReportAnalytics(application: Application) {
    // Reuse your app's existing instance instead if analytics is already initialized.
    private val amplitude: Amplitude? = AmplitudeConfig.API_KEY
        .takeIf { it.isNotBlank() }
        ?.let { key ->
            Amplitude(key, application.applicationContext) {
                serverZone = AmplitudeConfig.SERVER_ZONE
                autocapture = emptySet()
                enableAutocaptureRemoteConfig = false
            }
        }

    val configured: Boolean get() = amplitude != null
    fun deviceId(): String? = amplitude?.getDeviceId()

    fun reportPreviewed(days: Int) {
        amplitude?.track("Report Previewed", reportProperties(days))
    }

    fun reportRangeChanged(days: Int) {
        amplitude?.track("Report Range Changed", reportProperties(days))
    }

    // Call after your existing authentication confirms the account, before its events.
    fun accountSignedIn(canonicalUserId: String, plan: String) {
        require(canonicalUserId.length >= 5) { "Use a canonical ID of at least five characters" }
        amplitude?.setUserId(canonicalUserId)
        amplitude?.identify(Identify().set("plan", plan))
    }

    // Call only after confirmed logout, never for a loading or anonymous session.
    fun accountSignedOut() {
        amplitude?.reset()
    }

    fun flush() {
        amplitude?.flush()
    }

    private fun reportProperties(days: Int): Map<String, Any> = mapOf(
        "report_type" to "usage",
        "days" to days,
        "platform" to "Android",
    )
}

app/src/main/java/com/example/amplitudereports/MainActivity.kt

app/src/main/java/com/example/amplitudereports/MainActivity.kt
package com.example.amplitudereports

import android.app.Activity
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowInsets
import android.widget.Button
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView

class MainActivity : Activity() {
    private val analytics: ReportAnalytics
        get() = (application as ReportsApplication).analytics
    private var days = 7
    private var opened = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        days = savedInstanceState?.getInt("days", 7) ?: 7
        opened = savedInstanceState?.getBoolean("opened", false) ?: false
        val padding = (24 * resources.displayMetrics.density).toInt()
        val column = LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            setPadding(padding, padding, padding, padding)
        }
        val scroll = ScrollView(this).apply { addView(column) }
        scroll.setOnApplyWindowInsetsListener { view: View, insets: WindowInsets ->
            if (Build.VERSION.SDK_INT >= 30) {
                val safe = insets.getInsets(
                    WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout(),
                )
                view.setPadding(safe.left, safe.top, safe.right, safe.bottom)
            } else if (Build.VERSION.SDK_INT >= 28) {
                val cutout = insets.displayCutout
                view.setPadding(
                    maxOf(insets.systemWindowInsetLeft, cutout?.safeInsetLeft ?: 0),
                    maxOf(insets.systemWindowInsetTop, cutout?.safeInsetTop ?: 0),
                    maxOf(insets.systemWindowInsetRight, cutout?.safeInsetRight ?: 0),
                    maxOf(insets.systemWindowInsetBottom, cutout?.safeInsetBottom ?: 0),
                )
            } else {
                view.setPadding(
                    insets.systemWindowInsetLeft, insets.systemWindowInsetTop,
                    insets.systemWindowInsetRight, insets.systemWindowInsetBottom,
                )
            }
            insets
        }
        setContentView(scroll)
        scroll.requestApplyInsets()
        column.addView(TextView(this).apply {
            text = "Usage reports"
            textSize = 28f
        })
        val status = TextView(this).apply {
            text = if (analytics.configured) "Analytics configured; verify delivery separately"
                else "Analytics off: set the project API key"
        }
        column.addView(status)
        val report = TextView(this)
        fun render() {
            report.text = if (opened) "$days-day report: ${days * 12} sample actions"
                else "Open the sample report"
        }
        render()
        column.addView(report)
        column.addView(Button(this).apply {
            text = "Open report"
            setOnClickListener {
                opened = true
                render()
                analytics.reportPreviewed(days)
            }
        })
        column.addView(Button(this).apply {
            text = "Change date range"
            setOnClickListener {
                days = if (days == 7) 30 else 7
                opened = true
                render()
                analytics.reportRangeChanged(days)
            }
        })
        column.addView(Button(this).apply {
            text = "Flush and show device ID"
            setOnClickListener {
                analytics.flush()
                status.text = "Device ID: ${analytics.deviceId() ?: "not available yet"}; flush requested"
            }
        })
    }

    override fun onSaveInstanceState(outState: Bundle) {
        outState.putInt("days", days)
        outState.putBoolean("opened", opened)
        super.onSaveInstanceState(outState)
    }
}

Hand it to your agent

Use this prompt with your coding agent.

Agent prompt

Agent prompt
Follow https://amplitude.com/build/android-product-analytics. Build the complete fresh Android Views sample with its pinned Gradle/AGP and com.amplitude:analytics-android:1.35.0 setup, or merge the SDK changes into the existing app while preserving its supported build configuration, routes, authentication, permissions and other instrumentation. Keep generated wrapper launchers/JAR and local SDK settings. Initialize one client in Application.onCreate with application context, register the Application in the manifest, and reuse an existing client when present. Pair project key and region. Keep the sample's autocapture empty and remote autocapture configuration disabled. Track Report Previewed and Report Range Changed with report_type, days and platform. Connect accountSignedIn only to verified existing auth; do not invent login. Preserve first-login device identity; reset after confirmed logout and before a confirmed account switch, never on loading/null auth. Keep account events behind resolved authentication. Retain the selected range on Activity recreation without extra report events. Use the diagnostic device ID/flush button only for development. Distinguish compilation, device behavior, queued events, actual ingestion and processed identity outcomes in the result. Never claim a flush or mocked check proves delivery or persistence.

What this unlocks

Product analytics compares Report Previewed and Report Range Changed by days to see which report ranges people use.

Feature flags can support a separate report-layout rollout once you add its mobile SDK and evaluation flow.

Session replay adds a separate visual investigation path after you install and verify replay capture for the app.

Next steps

Amplitude is the leading all-in-one analytics platform for building better products. Product and engineering teams use Amplitude to connect product analytics and web analytics with session replay, heatmaps, experimentation, feature flags, and guides and surveys in one unified workflow, with AI agents working across all of it. Learn how Amplitude is building self-improving products with Wave, helping teams turn product data into PRs.

Ship the instrumentation, then read the data

Once report events are flowing, compare report ranges and follow the actions that lead to repeat use.