# Google Play Developer Reporting

> Export Android vitals (crash rate, ANR rate, slow start, LMK) from the Google Play Developer Reporting API to Last9 as OpenTelemetry metrics.

Source: https://last9.io/docs/integrations/others/google-play-reporting/

Export Android vitals from the [Google Play Developer Reporting API](https://developers.google.com/play/developer/reporting) to Last9 as OpenTelemetry metrics. Covers crash rate, ANR rate, slow start, excessive wakeups, stuck wakelocks, Low Memory Kill rate, and hourly error counts.

**Data freshness:** Daily metrics lag 24–48 hours; hourly error counts lag ~2–4 hours. These are Google-side constraints.

## Prerequisites

- Python 3.13+ or Docker
- Google Play Console service account with **View app information and download bulk reports (read-only)** permission
- Last9 account with OTLP ingestion enabled

## Google Play Console Setup

1. Go to **Play Console → Setup → API access**
2. Link or create a Google Cloud project
3. Create a service account (or use an existing one)
4. Grant the service account **View app information and download bulk reports (read-only)**
5. Download the JSON key file — store it securely, never commit it

## Quick Start

```bash
git clone https://github.com/last9/opentelemetry-examples
cd opentelemetry-examples/python/google-play-reporting

cp .env.example .env
# Edit .env with your values

export SERVICE_ACCOUNT_JSON_PATH=/path/to/your/service-account.json
docker compose up
```

## Configuration

| Variable                         | Description                                              | Default                             |
| -------------------------------- | -------------------------------------------------------- | ----------------------------------- |
| `GOOGLE_APPLICATION_CREDENTIALS` | Path to service account JSON inside container            | `/run/secrets/service-account.json` |
| `ANDROID_PACKAGE_NAMES`          | Comma-separated package names to monitor                 | `com.example.app`                   |
| `POLL_INTERVAL_SECONDS`          | Poll frequency in seconds                                | `1800`                              |
| `POLL_DAYS_BACK`                 | Days of history to fetch each poll                       | `30`                                |
| `ENABLE_HOURLY`                  | Also poll crash/ANR at hourly granularity (~2–4h lag)    | `false`                             |
| `ANDROID_DIMENSIONS`             | Comma-separated dimensions (e.g. `versionCode,apiLevel`) | _(aggregate)_                       |
| `OTLP_ENDPOINT`                  | Last9 OTLP endpoint                                      | `https://otlp.last9.io`             |
| `OTLP_HEADERS`                   | Auth headers (`Authorization=Basic <token>`)             | —                                   |
| `OTEL_SERVICE_NAME`              | Service name shown in Last9                              | `google-play-reporting`             |

> **Dimension note:** Google may return empty rows for per-dimension queries on low-traffic apps.
> Start with `ANDROID_DIMENSIONS=` (empty) and only enable dimensions once you confirm aggregate data flows.

## Metrics Exported

### Crash rate

| Metric                                     | Description                         |
| ------------------------------------------ | ----------------------------------- |
| `android.vitals.crash_rate`                | Daily crash rate                    |
| `android.vitals.crash_rate_7d`             | 7-day user-weighted crash rate      |
| `android.vitals.crash_rate_28d`            | 28-day user-weighted crash rate     |
| `android.vitals.user_perceived_crash_rate` | Foreground crash rate               |
| `android.vitals.crash_distinct_users`      | Users in the crash rate denominator |

### ANR rate

| Metric                                   | Description                       |
| ---------------------------------------- | --------------------------------- |
| `android.vitals.anr_rate`                | Daily ANR rate                    |
| `android.vitals.anr_rate_7d`             | 7-day user-weighted ANR rate      |
| `android.vitals.anr_rate_28d`            | 28-day user-weighted ANR rate     |
| `android.vitals.user_perceived_anr_rate` | Foreground ANR rate               |
| `android.vitals.anr_distinct_users`      | Users in the ANR rate denominator |

### Slow start

| Metric                               | Description                 |
| ------------------------------------ | --------------------------- |
| `android.vitals.slow_start_rate`     | Fraction of slow app starts |
| `android.vitals.slow_start_rate_7d`  | 7-day slow start rate       |
| `android.vitals.slow_start_rate_28d` | 28-day slow start rate      |

### Slow rendering

| Metric                                     | Description                     |
| ------------------------------------------ | ------------------------------- |
| `android.vitals.slow_rendering_rate_20fps` | Sessions rendering below 20 FPS |
| `android.vitals.slow_rendering_rate_30fps` | Sessions rendering below 30 FPS |

### Battery & memory

| Metric                                          | Description                                      |
| ----------------------------------------------- | ------------------------------------------------ |
| `android.vitals.excessive_wakeup_rate`          | Sessions with >10 AlarmManager wakeups/hour      |
| `android.vitals.stuck_background_wakelock_rate` | Sessions with a background wakelock held >1 hour |
| `android.vitals.lmk_rate`                       | Active-use Low Memory Kill rate                  |

### Error counts (hourly)

| Metric                                | Description                    |
| ------------------------------------- | ------------------------------ |
| `android.vitals.error_report_count`   | Hourly error count (~2–4h lag) |
| `android.vitals.error_distinct_users` | Distinct users with errors     |

## Labels

Every data point carries these labels:

| Label                  | Present on                            | Example                  |
| ---------------------- | ------------------------------------- | ------------------------ |
| `android_package`      | all metrics                           | `com.example.app`        |
| `android_date`         | all metrics                           | `2026-02-24`             |
| `android_report_type`  | error count only                      | `CRASH` or `ANR`         |
| `android_start_type`   | slow start only                       | `COLD`, `WARM`, or `HOT` |
| `android_version_code` | when `ANDROID_DIMENSIONS` includes it | `142`                    |
| `android_api_level`    | when `ANDROID_DIMENSIONS` includes it | `34`                     |

## Dashboard Queries

All metrics carry historical timestamps (the measurement date), not the current time. Set your Last9 dashboard time range to **Last 30 days**.

### Stat panels (current value)

Use `last_over_time` to avoid staleness returning 0 — each `android_date` series has one historical data point that goes stale after ~5 minutes:

```promql
# Crash rate (7d rolling)
max by (android_package) (last_over_time(android_vitals_crash_rate_7d_ratio{android_package="$package"}[30d]))

# ANR rate (7d rolling)
max by (android_package) (last_over_time(android_vitals_anr_rate_7d_ratio{android_package="$package"}[30d]))

# User-perceived crash rate
max by (android_package) (last_over_time(android_vitals_user_perceived_crash_rate_ratio{android_package="$package"}[30d]))

# Total error count
sum by (android_package) (last_over_time(android_vitals_error_report_count{android_package="$package"}[30d]))
```

### Time series panels (trends)

Use `max by` to collapse the `android_date` dimension — without it, each date creates a separate series in the legend:

```promql
# Daily crash rate trend
max by (android_package) (android_vitals_crash_rate_ratio{android_package="$package"})

# Daily ANR rate trend
max by (android_package) (android_vitals_anr_rate_ratio{android_package="$package"})

# Hourly error count by type (CRASH vs ANR)
sum by (android_package, android_report_type) (android_vitals_error_report_count{android_package="$package"})

# Slow start rate
max by (android_package) (android_vitals_slow_start_rate_ratio{android_package="$package"})

# Battery & memory
max by (android_package) (android_vitals_excessive_wakeup_rate_ratio{android_package="$package"})
max by (android_package) (android_vitals_stuck_background_wakelock_rate_ratio{android_package="$package"})
max by (android_package) (android_vitals_lmk_rate_ratio{android_package="$package"})
```

**Dashboard variable:** Add `$package` as a label values variable:

```
label_values(android_vitals_crash_rate_ratio, android_package)
```

## Verification

After starting the collector, check the logs for a successful export line:

```
INFO Exported 582 data points (32 metrics) to Last9
```

Then check Last9 for metrics with the prefix `android_vitals_*`.

## Troubleshooting

**`slowRenderingRateMetricSet` returns 403**

The service account is missing the slow rendering permission. Go to **Play Console → Setup → API access**, find the service account, and verify **View app information and download bulk reports (read-only)** is explicitly checked. The collector will automatically start exporting slow rendering metrics on the next poll after the permission is granted.

**Stat panels show 0**

The metrics carry historical timestamps. At "now", all data points are stale. Use `last_over_time(...[30d])` as shown in the query examples above.

**400 Bad Request on startup with `POLL_DAYS_BACK=1`**

Google's daily API requires `start_time < end_time`. With 1 day back, `start_time == end_time` when the freshness date is today. Use `POLL_DAYS_BACK=2` or higher.

**No data for dimensions**

Google may return empty rows for per-dimension queries on low-traffic apps. Start with `ANDROID_DIMENSIONS=` (empty) to confirm aggregate data flows first.

---

For questions, reach out on [Discord](https://discord.com/invite/Q3p2EEucx9) or [Email](mailto:support@last9.io).
