Chris Tam · Last updated 26 October 2026
Android preview links can launch a fresh activity or arrive while it is already running. I handle both paths and keep guide screen targeting separate from analytics event names.
In this tutorial, I connect Amplitude guides and surveys to a native Views report screen. The complete sample includes one analytics client, the engagement plugin, a stable screen name, and a tagged button for targeting.
A UsageReport screen with a report-open view tag, report action events, and a preview-link handler. You’ll create a guide or survey in Amplitude and preview it on the app before publishing the experience.
Prerequisites
- Android Studio, Android SDK platform 36 and Build Tools 36.0.0, JDK 17, and an API 24+ device or emulator. This Views sample uses AGP 9.2.0 and Gradle 9.4.1.
- A project API key from Organization settings > API Keys and its matching US or EU region.
- Access to guides and surveys and a mobile app configured in the project. Use an active internet connection for loading experiences. Set the project’s mobile URL scheme for device previews.
No Amplitude account yet? Sign up for free to create a project, and compare plans for the product access and limits your team needs.
1. Create the Android project
I create an Android Studio No Activity project named AmplitudeReports, with Kotlin DSL, package com.example.amplitudereports, and minimum SDK 24. Keep the generated gradlew, gradlew.bat, gradle-wrapper.jar, and machine-specific local.properties. Every custom file is included below; no earlier tutorial is required.
For an existing app, merge the additions into its current application and screen. Preserve build variants, supported plugin versions, authentication, routes, permissions, and instrumentation. Keep one analytics instance across all Android integrations. The complete files describe this fresh sample.
I pin analytics-android 1.35.0 and amplitude-engagement-android 4.1.0. The Android engagement plugin connects to an existing analytics instance. API 24 is the guides and surveys floor.
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
plugins {
id("com.android.application") version "9.2.0" apply false
}gradle.properties
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=truegradle/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/distsapp/build.gradle.kts
plugins {
id("com.android.application")
}
android {
namespace = "com.example.amplitudereports"
compileSdk = 36
defaultConfig {
applicationId = "com.example.amplitudereports"
minSdk = 24
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")
implementation("com.amplitude:amplitude-engagement-android:4.1.0")
}2. Initialize once in the application
I register an Application subclass and initialize with application context. Fill API_KEY with the project key and choose ServerZone.EU for an EU project. An empty key leaves this sample inactive. Mobile project keys ship in the app; management secrets stay on your server.
I add getPlugin() to the same analytics client. The plugin manages boot and shares analytics identity; do not boot a second standalone client or forward the same report event manually. Both SDKs use the project’s region.
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" android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="amp-report-demo" />
</intent-filter>
</activity>
</application>
</manifest>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
package com.example.amplitudereports
import android.app.Application
import com.amplitude.android.Amplitude
import com.amplitude.android.engagement.AmplitudeEngagement
import com.amplitude.android.engagement.AmplitudeInitOptions
import com.amplitude.android.engagement.AmplitudeServerZone
import com.amplitude.core.ServerZone
class ReportsApplication : Application() {
lateinit var analytics: ReportAnalytics
private set
var engagement: AmplitudeEngagement? = null
private set
override fun onCreate() {
super.onCreate()
val client = AmplitudeConfig.API_KEY.takeIf { it.isNotBlank() }?.let { key ->
Amplitude(key, applicationContext) {
serverZone = AmplitudeConfig.SERVER_ZONE
autocapture = emptySet()
enableAutocaptureRemoteConfig = false
}
}
if (client != null) {
engagement = AmplitudeEngagement(
context = applicationContext,
apiKey = AmplitudeConfig.API_KEY,
options = AmplitudeInitOptions(serverZone =
if (AmplitudeConfig.SERVER_ZONE == ServerZone.EU) AmplitudeServerZone.EU
else AmplitudeServerZone.US),
)
client.add(engagement!!.getPlugin())
}
analytics = ReportAnalytics(client)
}
// Main-thread callbacks from confirmed auth. Loading auth is not logout.
fun accountSignedIn(userId: String, plan: String) {
engagement?.closeAll()
analytics.accountSignedIn(userId, plan)
}
fun accountSignedOut() {
engagement?.closeAll()
analytics.accountSignedOut()
}
}3. Connect report events and account changes
I keep explicit report events in one adapter. In an authenticated app, call ReportsApplication.accountSignedIn from verified authentication before enabling report actions. A null or loading account is not logout. On a confirmed account switch, call accountSignedOut before accountSignedIn for the next account. The sample has no login UI; these are hooks for your existing authentication.
app/src/main/java/com/example/amplitudereports/ReportAnalytics.kt
package com.example.amplitudereports
import com.amplitude.android.Amplitude
import com.amplitude.core.events.Identify
class ReportAnalytics(private val amplitude: Amplitude?) {
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 account lifecycle hooks on the main thread. closeAll dismisses the current experience before switching identity; analytics.reset runs only after confirmed logout. It issues a new device identity. Keep actions disabled while authentication is unresolved, and preserve your existing deep-link routes alongside the preview handler.
4. Build the report screen
I keep seven- and 30-day report state across activity recreation without emitting another report event from onCreate. The platform Views UI handles system bars and display cutouts, including landscape. The device-ID and flush control is for development; it requests an upload without proving delivery. Remove it from your production screen.
I call screen("UsageReport") when the screen resumes and tag the Open report button as report-open. Preview intents are processed after the screen is ready for both a fresh launch and onNewIntent. Replace amp-report-demo in the manifest with the exact mobile URL scheme configured for this project; rebuild and reinstall afterward.
app/src/main/java/com/example/amplitudereports/MainActivity.kt
package com.example.amplitudereports
import android.content.Intent
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 val app: ReportsApplication get() = application as ReportsApplication
private var days = 7
private var opened = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) pendingPreview = intent
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 {
tag = "report-open"
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 onResume() {
super.onResume()
app.engagement?.screen("UsageReport")
pendingPreview?.let { app.engagement?.handleLinkIntent(it) }
pendingPreview = null
}
private var pendingPreview: Intent? = null
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
pendingPreview = intent
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt("days", days)
outState.putBoolean("opened", opened)
super.onSaveInstanceState(outState)
}
}5. Configure and run the development check
Set the scheme under Settings > Projects > your project > General > URL scheme (mobile). In Guides and Surveys > App Management, add the Android app. Create a guide or survey for the UsageReport screen. Use report-open when pinning a step to the button, and Report Previewed if the experience uses that event as its trigger. Configure the content, audience, and display rules. Save it for preview first; publishing the app code does not publish the guide. Use the mobile preview QR code or deep link on the device with this app installed.
Sync Gradle, then run these commands from the sample project. On Windows, use gradlew.bat.
Terminal
./gradlew :app:assembleDebug
./gradlew :app:installDebug
adb shell am start -n com.example.amplitudereports/.MainActivityVerify it worked
- Build and install the app, use a development key, and open the report. Tap the Amplitude logo in mobile preview to open the Super Debugger, then use its Details and Setup views to confirm the screen is UsageReport and the Report Previewed trigger arrives. Verify a pinned step targets report-open and can be dismissed.
- Open the preview link once with the app fully closed and again while MainActivity is running. Both paths should open the intended preview. Check normal application links still work after you merge the handler into your existing router.
- Confirm the configured audience and screen rules with synthetic signed-in and signed-out identities. Close the experience, complete confirmed logout, and check the next account cannot see the previous account’s experience state. Do not use reset(guideKey,stepIndex) as an identity reset.
- Inspect ingestion counts in Data > Sources > Ingestion Debugger, then the individual Report Previewed and Report Range Changed events in Users > User Profiles using the displayed device ID. Separately inspect the guide’s or survey’s results after a real development interaction. Local screen rendering does not establish targeting, delivery, or processed responses.
Troubleshooting
The preview link opens nothing
Match the installed manifest scheme to the project’s mobile URL scheme and reinstall after changing it. Preserve both initial intent and onNewIntent paths.
The guide is not shown on the report
Confirm the exact UsageReport screen target, trigger, audience, and saved preview. SDK initialization alone does not make an experience eligible.
A pinned step misses the button
Use the exact report-open view tag and confirm the tagged view is visible before the step is displayed. Keep accessibility labels meaningful.
Events or experiences appear twice
Reuse the existing analytics client, add the plugin once, and remove manual boot or duplicate forwardEvent calls for this plugin integration.
Frequently asked questions
Do I call boot with this plugin?
No. Adding the plugin integrates initialization with analytics. The standalone boot flow is a different integration path.
Does the preview link replace app navigation?
No. Merge preview handling with your existing links and router. Keep normal application navigation intact.
Full code reference
10 files. Keep the Android Studio wrapper launchers/JAR and local SDK configuration alongside these custom files.
// 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
plugins {
id("com.android.application") version "9.2.0" apply false
}
// gradle.properties
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
// 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
plugins {
id("com.android.application")
}
android {
namespace = "com.example.amplitudereports"
compileSdk = 36
defaultConfig {
applicationId = "com.example.amplitudereports"
minSdk = 24
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")
implementation("com.amplitude:amplitude-engagement-android:4.1.0")
}
// 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" android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="amp-report-demo" />
</intent-filter>
</activity>
</application>
</manifest>
// 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
package com.example.amplitudereports
import android.app.Application
import com.amplitude.android.Amplitude
import com.amplitude.android.engagement.AmplitudeEngagement
import com.amplitude.android.engagement.AmplitudeInitOptions
import com.amplitude.android.engagement.AmplitudeServerZone
import com.amplitude.core.ServerZone
class ReportsApplication : Application() {
lateinit var analytics: ReportAnalytics
private set
var engagement: AmplitudeEngagement? = null
private set
override fun onCreate() {
super.onCreate()
val client = AmplitudeConfig.API_KEY.takeIf { it.isNotBlank() }?.let { key ->
Amplitude(key, applicationContext) {
serverZone = AmplitudeConfig.SERVER_ZONE
autocapture = emptySet()
enableAutocaptureRemoteConfig = false
}
}
if (client != null) {
engagement = AmplitudeEngagement(
context = applicationContext,
apiKey = AmplitudeConfig.API_KEY,
options = AmplitudeInitOptions(serverZone =
if (AmplitudeConfig.SERVER_ZONE == ServerZone.EU) AmplitudeServerZone.EU
else AmplitudeServerZone.US),
)
client.add(engagement!!.getPlugin())
}
analytics = ReportAnalytics(client)
}
// Main-thread callbacks from confirmed auth. Loading auth is not logout.
fun accountSignedIn(userId: String, plan: String) {
engagement?.closeAll()
analytics.accountSignedIn(userId, plan)
}
fun accountSignedOut() {
engagement?.closeAll()
analytics.accountSignedOut()
}
}
// app/src/main/java/com/example/amplitudereports/ReportAnalytics.kt
package com.example.amplitudereports
import com.amplitude.android.Amplitude
import com.amplitude.core.events.Identify
class ReportAnalytics(private val amplitude: Amplitude?) {
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
package com.example.amplitudereports
import android.content.Intent
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 val app: ReportsApplication get() = application as ReportsApplication
private var days = 7
private var opened = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) pendingPreview = intent
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 {
tag = "report-open"
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 onResume() {
super.onResume()
app.engagement?.screen("UsageReport")
pendingPreview?.let { app.engagement?.handleLinkIntent(it) }
pendingPreview = null
}
private var pendingPreview: Intent? = null
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
pendingPreview = intent
}
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
Follow https://amplitude.com/build/android-guides-surveys. Build the complete native Kotlin Views sample, or merge it into the existing app while preserving authentication, navigation, build variants, and instrumentation. Keep one analytics client initialized with application context in Application.onCreate. Match key and region. Use the exact pinned dependencies shown on the page. Keep report events and properties unchanged. Connect verified auth callbacks; reset only after confirmed logout, never from loading or anonymous state. Add the engagement plugin once to the existing analytics client, without manual boot or duplicate event forwarding. Use API 24+, match both SDK regions, screen UsageReport, and tag report-open. Merge initial-launch and running-activity preview links with the existing router. Close experiences before confirmed account changes. Report compilation, local tests, real-device behavior, and live-service outcomes separately. Do not claim a build or stubbed test proves delivery, experiment analysis, masking, or guide targeting.What this unlocks
Guides and surveys can explain the UsageReport screen or collect feedback after Report Previewed. Product analytics lets you compare report usage after the experience is delivered.
Next steps
Reference docs
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.