Chris Tam · Last updated 26 October 2026
Android activity recreation and asynchronous SDK initialization can race with a consent change. I keep replay consent in the application, start capture only for the foreground demo screen, and cancel pending starts when consent changes.
In this tutorial, I add Amplitude session replay to a native Views app. The sample starts with capture stopped and includes the complete analytics and replay setup.
A report screen that requests replay capture only after consent, stops it when the screen pauses, and uses conservative masking plus an explicit mask tag. You’ll check an actual replay before using the setup with real data.
Prerequisites
- Android Studio, Android SDK platform 36 and Build Tools 36.0.0, JDK 17, and an API 23+ 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.
- A project enabled for session replay, available replay quota, and a development recording containing only synthetic test content.
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 23. 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.
The Unified SDK supplies analytics, experiment, and replay together. I pin unified-android 1.0.0, which bundles analytics 1.33.0, experiment 1.17.0, and replay 0.31.0. When migrating, remove separate analytics/experiment/replay dependencies and their initialization or plugin registration. The explicit coroutines dependency supports the background initialization wait; OkHttp 4.12.0 follows the Unified docs’ dependency workaround.
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 = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
implementation("com.amplitude:unified-android:1.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
implementation("com.squareup.okhttp3:okhttp:4.12.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 disable experiment, set replay autoStart to false, keep WebView capture off, and request conservative masking. sampleRate=1.0 is for this synthetic development check; lower the rate before production. I disable replay remote configuration for this controlled check so server sampling and masking overrides cannot change the local policy. Project limits still apply. Consent controls are separate from sampling; review the remote policy before enabling remote configuration in production.
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
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.unified.AmplitudeUnified
import com.amplitude.android.sessionreplay.config.PrivacyConfig
import com.amplitude.android.sessionreplay.config.MaskLevel
class ReportsApplication : Application() {
lateinit var analytics: ReportAnalytics
private set
lateinit var replay: ReportReplay
private set
override fun onCreate() {
super.onCreate()
val client = AmplitudeConfig.API_KEY.takeIf { it.isNotBlank() }?.let { key ->
AmplitudeUnified(key, applicationContext) {
analytics {
serverZone = AmplitudeConfig.SERVER_ZONE
autocapture = emptySet()
enableAutocaptureRemoteConfig = false
}
experiment { enabled = false }
sessionReplay {
sampleRate = 1.0 // Development verification; lower before production.
enableRemoteConfig = false // Keep this synthetic check on its local privacy policy.
autoStart = false
captureWebViews = false
privacyConfig = PrivacyConfig(maskLevel = MaskLevel.CONSERVATIVE)
}
}
}
analytics = ReportAnalytics(client)
replay = ReportReplay(client)
}
// Stop capture before an account transition or a sensitive screen appears.
fun accountSignedIn(userId: String, plan: String) {
replay.setConsent(false)
analytics.accountSignedIn(userId, plan)
}
fun accountSignedOut() {
replay.setConsent(false)
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 the controller and account hooks on the main thread. I stop capture before account transitions and revoke this demo’s consent. The controller also cancels an initialization wait that finishes after revocation or screen pause. In a real app, connect this to your consent manager and stop replay before navigating to screens outside the approved capture area. This demo keeps consent only for the current process.
app/src/main/java/com/example/amplitudereports/ReportReplay.kt
package com.example.amplitudereports
import android.os.Handler
import android.os.Looper
import com.amplitude.unified.AmplitudeUnified
import java.util.concurrent.Executors
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
// Main-thread consent and screen-lifecycle controller; consent starts false per process.
class ReportReplay(private val amplitude: AmplitudeUnified?) {
private val worker = Executors.newSingleThreadExecutor()
private val main = Handler(Looper.getMainLooper())
var consent = false
private set
private var foreground = false
private var generation = 0
fun setConsent(allowed: Boolean) { consent = allowed; applyState() }
fun foreground(active: Boolean) { foreground = active; applyState() }
private fun applyState() {
val token = ++generation
if (!consent || !foreground) {
amplitude?.sessionReplay?.stop()
return
}
worker.execute {
try {
val sdk = amplitude ?: return@execute
runBlocking { withTimeout(10000) { sdk.isBuilt.await() } }
main.post {
if (token == generation && consent && foreground) sdk.sessionReplay?.start()
}
} catch (_: Exception) {
// Leave capture stopped when initialization fails.
}
}
}
}Why. Stop() prevents subsequent capture; it does not erase recordings already uploaded. This checkbox controls replay only. Apply your separate analytics consent policy before initializing or tracking analytics.
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.
The checkbox grants replay consent for this demo screen. The report view and synthetic input also have amp-mask tags. The input contains SYNTHETIC-PRIVATE-12345 for the recording check; remove this test field from production. Conservative masking requests masking for text views and inputs; verify the actual recording with synthetic sensitive text before allowing real data.
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.EditText
import android.widget.CheckBox
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)
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(EditText(this).apply {
setText("SYNTHETIC-PRIVATE-12345")
hint = "Synthetic privacy test field"
tag = "amp-mask"
})
report.tag = "amp-mask"
column.addView(report)
column.addView(CheckBox(this).apply {
text = "Allow replay on this demo screen"
isChecked = app.replay.consent
setOnCheckedChangeListener { _, checked -> app.replay.setConsent(checked) }
})
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 onResume() { super.onResume(); app.replay.foreground(true) }
override fun onPause() { app.replay.foreground(false); super.onPause() }
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt("days", days)
outState.putBoolean("opened", opened)
super.onSaveInstanceState(outState)
}
}5. Configure and run the development check
Enable capture for the correct project in Session Replay Settings and check available quota. Start with a development project and synthetic text only. The sample defaults to capture stopped on process start; check the consent box only after the screen is ready.
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 run with a development key. Before consent, interact with the screen and confirm no replay capture begins. Grant consent, open the report, change range, then revoke consent and interact again. Test backgrounding and a confirmed logout during initialization.
- Use synthetic distinctive text in a report and input field to check the masking policy. Open the captured replay in Amplitude and inspect frames before and after consent, navigation, and revocation. Confirm text is unreadable where required. A mask tag or a compiled configuration is not proof of masking in a recording.
- Check Data > Sources > Ingestion Debugger counts and find Report Previewed and Report Range Changed under Users > User Profiles by device ID. Confirm replay linkage from the same test identity/session rather than relying on a local status label.
- Before production, validate your real Views, custom widgets, and sensitive navigation paths using synthetic content. Keep capture stopped for any screen whose policy you have not validated. Start with a low production sample rate and check project quota.
Troubleshooting
No replay appears
Check the project key, matching region, capture setting, quota, consent, and sampling configuration. The checkbox requests capture; it does not guarantee an eligible uploaded recording.
Capture continues on another screen
Call foreground(false) before leaving the approved screen and revoke consent before sensitive navigation. Keep the asynchronous generation check so a late initialization cannot restart capture.
Text appears that should be masked
Stop capture. Recheck the mask level and view tags, then reproduce with synthetic text in an actual recording. Do not fix a privacy issue by raising the sample rate.
Analytics works but replay does not
Verify a single Unified client owns both products and remove duplicate replay plugins. Keep captureWebViews false for this sample.
Frequently asked questions
Does disabling replay also disable analytics?
The replay controller stops replay capture. Report event calls remain separate; apply your analytics consent policy independently.
Can I approve masking from this code alone?
No. Check an actual recording containing synthetic sensitive content before enabling capture of real data. Repeat that check when sensitive views or SDK versions change.
Full code reference
11 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 = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
implementation("com.amplitude:unified-android:1.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
implementation("com.squareup.okhttp3:okhttp:4.12.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">
<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
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.unified.AmplitudeUnified
import com.amplitude.android.sessionreplay.config.PrivacyConfig
import com.amplitude.android.sessionreplay.config.MaskLevel
class ReportsApplication : Application() {
lateinit var analytics: ReportAnalytics
private set
lateinit var replay: ReportReplay
private set
override fun onCreate() {
super.onCreate()
val client = AmplitudeConfig.API_KEY.takeIf { it.isNotBlank() }?.let { key ->
AmplitudeUnified(key, applicationContext) {
analytics {
serverZone = AmplitudeConfig.SERVER_ZONE
autocapture = emptySet()
enableAutocaptureRemoteConfig = false
}
experiment { enabled = false }
sessionReplay {
sampleRate = 1.0 // Development verification; lower before production.
enableRemoteConfig = false // Keep this synthetic check on its local privacy policy.
autoStart = false
captureWebViews = false
privacyConfig = PrivacyConfig(maskLevel = MaskLevel.CONSERVATIVE)
}
}
}
analytics = ReportAnalytics(client)
replay = ReportReplay(client)
}
// Stop capture before an account transition or a sensitive screen appears.
fun accountSignedIn(userId: String, plan: String) {
replay.setConsent(false)
analytics.accountSignedIn(userId, plan)
}
fun accountSignedOut() {
replay.setConsent(false)
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/ReportReplay.kt
package com.example.amplitudereports
import android.os.Handler
import android.os.Looper
import com.amplitude.unified.AmplitudeUnified
import java.util.concurrent.Executors
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
// Main-thread consent and screen-lifecycle controller; consent starts false per process.
class ReportReplay(private val amplitude: AmplitudeUnified?) {
private val worker = Executors.newSingleThreadExecutor()
private val main = Handler(Looper.getMainLooper())
var consent = false
private set
private var foreground = false
private var generation = 0
fun setConsent(allowed: Boolean) { consent = allowed; applyState() }
fun foreground(active: Boolean) { foreground = active; applyState() }
private fun applyState() {
val token = ++generation
if (!consent || !foreground) {
amplitude?.sessionReplay?.stop()
return
}
worker.execute {
try {
val sdk = amplitude ?: return@execute
runBlocking { withTimeout(10000) { sdk.isBuilt.await() } }
main.post {
if (token == generation && consent && foreground) sdk.sessionReplay?.start()
}
} catch (_: Exception) {
// Leave capture stopped when initialization fails.
}
}
}
}
// 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.EditText
import android.widget.CheckBox
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)
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(EditText(this).apply {
setText("SYNTHETIC-PRIVATE-12345")
hint = "Synthetic privacy test field"
tag = "amp-mask"
})
report.tag = "amp-mask"
column.addView(report)
column.addView(CheckBox(this).apply {
text = "Allow replay on this demo screen"
isChecked = app.replay.consent
setOnCheckedChangeListener { _, checked -> app.replay.setConsent(checked) }
})
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 onResume() { super.onResume(); app.replay.foreground(true) }
override fun onPause() { app.replay.foreground(false); super.onPause() }
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-session-replay. 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. Start replay stopped, keep consent independent from sampling, cancel late starts after revocation, and stop capture before screen pause, account changes, and sensitive navigation. Keep conservative masking and WebView capture disabled. Validate actual recordings with synthetic content before allowing real data. 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
Session replay adds visual context to Report Previewed after capture, identity linkage, and masking are verified. Product analytics compares those report events by days.
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.