![image[1]-Monitoring Android Vitals with the Play Developer Reporting API For Windows 7,8,10,11-Winpcsoft.com](https://winpcsoft.com/wp-content/plugins/wp-fastest-cache-premium/pro/images/blank.gif)
At Aircall, we’ve built an automated reporting tool that pulls our Android Vitals metrics and delivers a daily health dashboard directly to Slack thanks to the Play Developer Reporting API. All of this is supported by a simple Gradle task.
Discover the API
The Google Play Developer Reporting API provides programmatic access to the same quality metrics you see in the Google Play Console. It covers the most important Android vitals: ANR rate, crash rate, slow startup rate, and stuck background wakelocks, the signals Google uses to determine whether your app meets bad behavior thresholds.
The API is organized around sets of metrics that you query with a timeline specification. Each metric set returns user-weighted daily, 7-day, and 28-day aggregates, giving you a complete picture from short-term regressions to long-term trends. The main advantage over console UI scraping is that you can automate the entire process and integrate it into your existing CI/CD pipeline.
The full API reference can be found in the official documentation.
Build with Play Vitals
Setting up the dependency
We decided to implement this as a Gradle task in ours Build logic Module. This keeps the reporting logic close to the project without polluting the app code. D'abord, let’s add the dependency:
# libs.versions.toml googleApiReporting = "v1beta1-rev20230803-2.0.0" buildLogic-plugin-google-play-developer-reporting = { module = "com.google.apis:google-api-services-playdeveloperreporting", version.ref = "googleApiReporting" }
Which can then be added build.gradle.kts from you Build logic Module:
dependencies { // API to get Google Play store vitals metrics implementation(libs.buildLogic.plugin.google.play.developer.reporting) }
Authenticate with a service account
To access the API, you need a Google Cloud service account with the Play Developer Reporting scope. We pass the credentials as an environment variable to keep secrets out of the repository. Le reporting The client initializes the API with these credentials:
class Reporting { private var reporting: Playdeveloperreporting init { val credentials = GoogleCredentials .fromStream(REPORTING_SERVICE_ACCOUNT.byteInputStream()) .createScoped("https://www.googleapis.com/auth/playdeveloperreporting") reporting = Playdeveloperreporting.Builder( GoogleNetHttpTransport.newTrustedTransport(), GsonFactory.getDefaultInstance(), HttpCredentialsAdapter(credentials), ).setApplicationName("reporting").build() } }
Query vital data
Each metric set has its own query endpoint. Here’s how we pulled the ANR stats with daily, weekly and monthly aggregates:
fun getAnrStats(): Pair<AnrStats?, UserPerceivedAnrStats?>? { val resName = "$BASE/anrRateMetricSet" val metrics = listOf( "anrRate", "anrRate7dUserWeighted", "anrRate28dUserWeighted", "userPerceivedAnrRate", "userPerceivedAnrRate7dUserWeighted", "userPerceivedAnrRate28dUserWeighted", ) val row = reporting.vitals().anrrate().query( resName, GooglePlayDeveloperReportingV1beta1QueryAnrRateMetricSetRequest() .setTimelineSpec(timelineSpec) .setMetrics(metrics), ).execute().rows[0] val map = row.metrics.associate { it.metric to it.decimalValue.value.toFloat() * 100 } return AnrStats( anrRate = map["anrRate"] ?: NOT_FOUND, anrRateWeek = map["anrRate7dUserWeighted"] ?: NOT_FOUND, anrRateMonth = map["anrRate28dUserWeighted"] ?: NOT_FOUND, ) to UserPerceivedAnrStats( userPerceivedAnrRate = map["userPerceivedAnrRate"] ?: NOT_FOUND, userPerceivedAnrRateWeek = map["userPerceivedAnrRate7dUserWeighted"] ?: NOT_FOUND, userPerceivedAnrRateMonth = map["userPerceivedAnrRate28dUserWeighted"] ?: NOT_FOUND, ) }
The same pattern applies to crash rate, slow boot rate, stuck wakelocks in the background, and error count. Each endpoint follows the same query structure, only the metric set name and metric names change. The Timeline specification defines the aggregation window. We query data that is anchored in the daily America/Los_Angeles Timezone (as required by the API):
val timelineSpec = GooglePlayDeveloperReportingV1beta1TimelineSpec() .setAggregationPeriod("DAILY") .setStartTime( GoogleTypeDateTime() .setTimeZone(losAngelesTimeZone) .setDay(startTimeLosAngeles.dayOfMonth) .setMonth(startTimeLosAngeles.monthValue) .setYear(startTimeLosAngeles.year), ) .setEndTime( GoogleTypeDateTime() .setTimeZone(losAngelesTimeZone) .setDay(endTimeLosAngeles.dayOfMonth) .setMonth(endTimeLosAngeles.monthValue) .setYear(endTimeLosAngeles.year), )
Modeling the data
We keep the data models simple. Each metric set is assigned to its own data class. Par exemple, here is the data class for the ANR metrics.
data class AnrStats( val anrRate: Float, val anrRateWeek: Float, val anrRateMonth: Float, ) : Stats()
Add health thresholds
Raw numbers are useful, but what the team really needs is a quick visual status. We’ve defined thresholds that align with Google’s levels of bad behavior. This gives us a traffic light system: green when we are safely below the thresholds, yellow when we are approaching, and red when we have exceeded Google’s limits for bad behavior.
const val ANR_RATE_LOWER_THRESHOLD = 0.25f // Green below const val ANR_RATE_UPPER_THRESHOLD = 0.47f // Red above private fun computeStatusEmoji( value: Float, lowerThreshold: Float, upperThreshold: Float, ): String { return when { value >= upperThreshold -> ":red_circle:" value < lowerThreshold -> ":large_green_circle:" else -> ":large_yellow_circle:" } }
Submitting the report to Slack
Now we are ready to create our Slack report and send it in our dedicated Slack channel. By using the Java Slack API, we are now able to have a Gradle task that can be triggered by our CI to deliver the report every morning.
fun sendSlackMessage(statsBlocks: ReportStatsBlocks, errorCounts: ErrorCounts?) { val slack = Slack.getInstance() slack.methods(SLACK_TOKEN).chatPostMessage { chatRequest -> chatRequest.channel(SLACK_REPORTS_CHANNEL) .username("Report bot").iconEmoji(":android:") .blocks { header { text("Report for $twoDaysAgo") } divider() section { fields { markdownText("*Period*") markdownText("*Day* *Week* *Month*") } } section { fields { statsBlocks.anrStatsBlock?.let { buildLine(it.title, it.body) } statsBlocks.crashStatsBlock?.let { buildLine(it.title, it.body) } markdownText("*Crash count*") plainText("${errorCounts?.crashCount} for ${errorCounts?.crashDistinctUsers} users") } } // ... slow start, stuck wakelocks } } }
Now we have everything to register our new Gradle reporting task in our TaskRegistry so we can run it in our CI environment
// Main Reporting task open class ReportingTask : DefaultTask() { private val reporting = Reporting() private val statsMapper = StatsMapper() @TaskAction fun report() { val anrStats = reporting.getAnrStats() val errorCounts = reporting.getErrorCount() ... val reportStatsBlocks = ReportStatsBlocks( anrStatsBlock = anrStats?.first?.let { statsMapper.map(it) }, ..., ) sendSlackMessage(statsBlocks = reportStatsBlocks, errorCounts = errorCounts) } } // Task registration private fun Project.registerReporting() { tasks.register( "reporting", ReportingTask::class.java, ) { task -> task.group = "notification" task.description = """ Send Slack report with vitals """.trimIndent() } }
Here is the final report we receive every morning on our Slack channel:
![image[2]-Monitoring Android Vitals with the Play Developer Reporting API For Windows 7,8,10,11-Winpcsoft.com](https://winpcsoft.com/wp-content/plugins/wp-fastest-cache-premium/pro/images/blank.gif)
Ours every morning #android-reports The Slack channel gets a clean health dashboard with all the important Android Vitals. Each metric shows its daily value, 7-day trend, and 28-day aggregate with color-coded status and trend arrows, allowing the team to assess app health at a glance without having to open the Play Console.
The entire tool lives within us Build logic Module as a single Gradle task with around 300 lines of code. No additional infrastructure, no external service to maintain, just a CI job to run ./gradlew reporting once a day. This approach gives us a high-level overview of the app health, but also gives stakeholders insight into the app’s behavior in production.
Don’t hesitate to ping me LinkedIn if you have any questions 🤓
Monitoring Android Vitals with the Play Developer Reporting API was originally published in Google Developer Experts on Medium, where people are continuing the discussion by highlighting and responding to this story.
