Turn Google Play Vitals into actionable insights — automatically.
This tool pulls your apps' stability and performance data (ANRs, crashes, slow starts, background wakelocks) plus the latest user reviews from the Google Play APIs, then uses an LLM (Google Gemini, or any local model) to write a plain-language report with trends and concrete recommendations — no dashboard digging required.
- 📊 One command, full summary — a single
./weekly_report.shfetches all the data and produces an AI-written Markdown report per app. - 🧠 Vitals + user reviews in one report — the AI correlates crash spikes with what users are actually complaining about.
- 💰 AdMob monetization monitoring — a standalone script tracks impressions, clicks, earnings, eCPM and fill rates per ad unit, with its own AI report (user-OAuth based).
- 🔀 Flexible timeframes — daily, weekly, or custom windows; reviews can be fetched as "latest N" or time-bounded.
- 🔒 No secrets stored — credentials live in your local
.env; nothing is committed. - 🧩 Extensible — designed so new data sources (ratings, subscriptions, etc.) can be added cleanly.
Once configured (see Setup Guide), a single run gives you everything:
./weekly_report.sh --reviewsThat one command: fetches 7 days of vitals + the latest user reviews for every app in your account, then generates a report like this per app:
# App Vitals & User Review Report: `com.example.app`
- Crash rate: 0% across the whole period ✅
- ANR spikes on Aug 4–5 (0.01%–0.0115%) — single-user, persistent issue
- Cold start 3%–8% — main optimization opportunity
- User feedback: "too many ads" (1★) posted same day as an ANR spike → ad-loading may be blocking startup
**Top recommendations:**
1. Move non-critical init tasks off the main thread
2. Delay ad loading until after first frame
3. Investigate ANR correlation with launch sequenceFull sample output: examples/example_report.md · Data structure: examples/example_data.json
- How It Works
- Prerequisites
- Setup Guide
- Usage
- Examples
- User Reviews Feature (Optional)
- AdMob Monitoring (Optional)
- Output
- Architecture & Extending
- Troubleshooting
- License
The system is composed of two independent scripts orchestrated by a shell helper:
-
fetch_data.py— Queries the Google Play Developer Reporting API for each of your apps and stores the raw metrics in a JSON file underdata/. -
generate_report.py— Reads the latest JSON data, formats it into a prompt, and sends it to an LLM (Gemini by default) to generate a Markdown report per app.
┌──────────────────────┐
│ Google Play API │
│ (Developer Reporting)│
└──────────┬───────────┘
│ daily vitals
▼
┌──────────────────────┐
│ fetch_data.py │
│ → data/vitals_*.json │
└──────────┬───────────┘
│ JSON data
▼
┌──────────────────────┐
│ generate_report.py │
│ (Gemini / local LLM) │
└──────────┬───────────┘
│ .md report per app
▼
┌──────────────────────┐
│ data/vitals_*_app.md │
└──────────────────────┘
Before using this tool you need:
- A Google Play Developer account with at least one published app.
- A Google Cloud project with the Play Developer Reporting API enabled.
- A service account (JSON key) with access to your Play Console data.
- Python 3.9+ installed.
- A Gemini API key (free tier available at aistudio.google.com) — or a local LLM like Ollama.
This is the most important step. Follow it carefully.
- Go to the Google Cloud Console.
- Click the project dropdown at the top and select New Project (or choose an existing one).
- Give it a name (e.g.,
play-vitals-monitor) and click Create.
- In your project, go to APIs & Services > Library.
- Search for Google Play Developer Reporting API.
- Click Enable.
- Go to APIs & Services > Credentials.
- Click + Create Credentials > Service Account.
- Give it a name (e.g.,
play-vitals-reader). - Click Done (skip granting roles for now — access is granted in Play Console).
- In the Service Accounts list, click on the account you just created.
- Go to the Keys tab.
- Click Add Key > Create New Key.
- Choose JSON and click Create.
- A
.jsonfile will be downloaded automatically — keep this file safe.
Where to place the file: Place the downloaded JSON key file in the root of this project directory:
<project_root>/service_account.jsonThe default
.env.exampleexpects the file to be namedservice_account.jsonin the project root. You can use a different path by updatingGOOGLE_APPLICATION_CREDENTIALSin.env.
The service account must be granted access to your apps inside the Google Play Console.
- Go to the Google Play Console.
- Navigate to Users and permissions (under Settings).
- Click Invite new user.
- Enter the service account email (it looks like
play-vitals-reader@your-project.iam.gserviceaccount.com). - Under App permissions, select all apps you want to monitor.
- Under Account permissions, grant at least:
- View app data (read-only) — this allows reading vitals data.
- Click Invite.
It may take a few minutes for the permissions to propagate.
Copy the example configuration and edit it:
cp .env.example .envEdit .env with your values:
# Path to your service account JSON key file
GOOGLE_APPLICATION_CREDENTIALS=service_account.json
# Your LLM API key (Gemini)
GEMINI_API_KEY=AIzaSyYourActualKeyHere
# Comma-separated list of package names to monitor (find these in Play Console)
PACKAGE_NAMES=com.example.app1,com.example.app2,com.example.app3
# Optional: Local LLM command (if not using Gemini)
# LOCAL_LLM_COMMAND="ollama run llama3"- Open the Google Play Console.
- Go to All apps — each app's package name is listed under its icon (e.g.,
com.example.transit). - You can also find it in the URL when viewing an app:
https://play.google.com/console/developers/.../app/com.example.transit/...
- Go to Google AI Studio.
- Click Get API Key.
- Create a new API key (free tier includes generous limits).
- Copy the key into
GEMINI_API_KEYin your.envfile.
pip install -r requirements.txtIt's recommended to use a virtual environment:
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt./weekly_report.shThis runs a 7-day report: fetch data, then generate LLM summaries.
./run_reports.sh 14 # Last 14 days
./run_reports.sh 30 # Last 30 days
./run_reports.sh 1 # Last 1 day (latest available)./weekly_report.sh --reviews # 7-day vitals + 7-day reviews (default)
./run_reports.sh 14 --reviews # 14-day vitals + 14-day reviews
./run_reports.sh 7 --reviews-count 20 # 20 latest reviews per app, no time filter
./run_reports.sh 7 --reviews --reviews-days 30 # 7-day vitals + 30-day reviewsBy default, when --reviews is used with vitals, reviews are filtered to the same timeframe as vitals (--days). Use --reviews-days to set an independent timeframe. Set --reviews-days 0 to fetch the latest N reviews without time filtering.
./run_reports.sh --reviews-only # Latest 50 reviews, no time filter
./run_reports.sh --reviews-only --reviews-days 7 # Reviews from last 7 days only
# Or directly, with more control:
python3 fetch_data.py --reviews-only --reviews-count 30 --reviews-days 14
python3 generate_report.pyWhen run in --reviews-only mode, the script only queries the Android Publisher API for user reviews, ignoring vitals entirely. The resulting data file is named reviews_TIMESTAMP.json instead of vitals_TIMESTAMP.json. The LLM report will contain a dedicated User Reviews Summary section.
If you prefer a local model (e.g., via Ollama), set the LOCAL_LLM_COMMAND in .env:
LOCAL_LLM_COMMAND="ollama run llama3"Then run:
python3 generate_report.py --localFetch data only:
python3 fetch_data.py --days 7Generate reports from an existing data file:
# Use the latest data file
python3 generate_report.py
# Use a specific data file
python3 generate_report.py --file data/vitals_20260511_164757.jsonSanitized example outputs are provided in the examples/ directory to help you understand what the tool produces:
| File | Description |
|---|---|
examples/example_report.md |
A sample AI-generated report (vitals + user reviews) with app names and user identities redacted |
examples/example_data.json |
A synthetic sample of the JSON data structure produced by fetch_data.py, with fictional values |
examples/example_admob_report.md |
A sample AI-generated AdMob monetization report, with IDs and figures fictionalized |
examples/example_admob_data.json |
A synthetic sample of the JSON produced by fetch_admob.py, with fictional values |
Privacy note: The examples are sanitized — app package names, user names, and metrics are fictional or redacted. Your real reports are stored locally in
data/, which is gitignored and never committed.
This tool can optionally fetch the latest user reviews from Google Play and include them in the AI report for a richer analysis that correlates user sentiment with app vitals.
When --reviews is passed, fetch_data.py calls the Google Play Android Publisher API (reviews.list) to retrieve the most recent user reviews for each app.
The reviews are stored alongside vitals in the same JSON file under a "reviews" key. When generate_report.py runs, the LLM prompt is extended to ask the AI to:
- Provide a dedicated
## User Reviews Summarysection in the report with key themes and notable quotes - Identify common praises and complaints
- Note sentiment trends (positive, negative, mixed)
- Correlate user feedback with vitals spikes where possible
- Suggest specific improvements based on actual user comments
The dedicated section ensures reviews are always visible in the output, even when the AI also integrates them into other parts of the report.
Use --reviews-only to fetch and analyze reviews without querying vitals at all. This is useful for quick check-ins or when you only care about user sentiment.
python3 fetch_data.py --reviews-only --reviews-count 30
python3 generate_report.pyThe output file is named reviews_TIMESTAMP.json to distinguish it from combined data files.
# Full pipeline with reviews (default 50 latest)
./weekly_report.sh --reviews
# Fetch vitals + latest 20 reviews
python3 fetch_data.py --days 7 --reviews --reviews-count 20The service account needs the androidpublisher API scope. This is covered by the "View app data (read-only)" permission already granted in the Play Console (step 2 of Setup). No additional API enablement is required beyond what's already done for vitals.
- Max
--reviews-countis 100 (API limit per page). - If
--reviewsis not passed, the report is generated from vitals only. - The reviews API requires the service account to have the
androidpublisherscope, which is separate from theplaydeveloperreportingscope used for vitals.
A standalone script, fetch_admob.py, monitors your ad monetization: impressions, clicks, earnings, eCPM, match rate and more — per app and per ad unit — and generates an AI-written monetization report with actionable recommendations (e.g., low fill rates, underperforming ad units, placement suggestions).
It is fully independent from the vitals/reviews scripts (no shared code, no shared config beyond the same .env file).
AdMob does NOT support service accounts. Unlike the Play Developer Reporting and Android Publisher APIs (which work with your service_account.json), the AdMob API requires user-based OAuth 2.0: a real Google Account with access to your AdMob account must authorize the tool once, generating a long-lived refresh token that the script reuses automatically.
Follow these steps once. They are fiddly — read all of them before starting.
- In Google Cloud Console, select your project.
- Go to APIs & Services > Library and enable the Google AdMob API (
admob.googleapis.com).
This is the step most people trip on. You must configure a consent screen even though this is a personal tool.
- Go to APIs & Services > OAuth consent screen.
- Choose External user type (required for any app using OAuth with a personal Google account; Internal is only available for Google Workspace organizations).
- Fill in the required fields: app name, support email, and developer contact email.
- Add yourself as a test user — see the caveat below.
- Save.
The tester caveat (critical):
- Unless your project is submitted for Google verification (which requires logos, privacy policy, review, and is meant for public apps), the consent screen is in "Testing" mode. In testing mode, only accounts you explicitly list as test users can authorize.
- You are the owner of the AdMob account, the Cloud project, and the Google account — but that is not enough. You must manually add your own email address to the test users list in the OAuth consent screen, or the authorization will fail with an
access_denied/ "app not verified" error. - Add your email under Audience > Test users > Add users, then save.
- Go to APIs & Services > Credentials > Create Credentials > OAuth client ID.
- Choose Desktop app as the application type (simplest for a local script — no redirect URIs needed).
- Click Create, then Download JSON. Save it somewhere you can reference (e.g.,
admob_oauth_client.jsonin the project root).
- Go to apps.admob.com > Settings > Users.
- Click Add user and enter the email of the Google account you will authorize with (the same one you added as a test user).
- Choose a role (e.g., "Read only" — this tool only reads).
- Accept the invite from that email account.
python3 admob_oauth_setup.py --client-json admob_oauth_client.jsonA browser opens → log in with the invited Google account → accept the consent. The script prints:
ADMOB_REFRESH_TOKEN=1//xxxx
ADMOB_CLIENT_ID=xxxx.apps.googleusercontent.com
ADMOB_CLIENT_SECRET=xxxx
Add these keys manually to your .env file. The script does not modify .env for you — copy the three lines into it yourself (see .env.example for the layout).
Heads-up: For unverified (testing-mode) apps, Google may revoke refresh tokens that go unused for ~6 months, or when the test user list changes. If you later hit an
invalid_grant/ 401 error, just re-run this step.
# Quick start: auto-discover apps & ad units, 7-day report + LLM summary
./run_admob.sh 7 --discover
# Fetch only (no LLM)
python3 fetch_admob.py --days 7 --discover --fetch-only
# LLM report from an existing JSON file
python3 fetch_admob.py --file data/admob_20260812_*.json
# Restrict to specific apps
python3 fetch_admob.py --days 7 --discover --packages com.example.app1,com.example.app2
# Use a local LLM instead of Gemini
python3 fetch_admob.py --days 7 --discover --localNotes:
- The script auto-discovers all linked apps and ad units via the API (
--discover), so you don't need to declare them. - Report period is limited to 31 days per request (API constraint). For longer periods, run in chunks.
- Money is reported in USD by default (
--currencyorADMOB_CURRENCYto change). Ratios (CTR, match rate) are 0–1 (1.0 = 100%).
Auto-discovery covers most cases. If you prefer to declare ad units explicitly (or restrict the report to specific units), create a JSON file like admob_ad_units.example.json:
{
"com.example.app": [
"ca-app-pub-1234567890123456/1234567890"
]
}Then run:
python3 fetch_admob.py --days 7 --ad-units-config admob_ad_units.jsonDeclared ad unit IDs are validated against the API — a typo raises a clear error listing the available units.
Per day, per ad unit: AD_REQUESTS, MATCHED_REQUESTS, IMPRESSIONS, CLICKS, ESTIMATED_EARNINGS, IMPRESSION_CTR, IMPRESSION_RPM (eCPM), MATCH_RATE, SHOW_RATE, plus per-ad-unit and per-app totals (CTR, eCPM, match rate).
All output goes to the data/ directory.
| File Pattern | Contents |
|---|---|
data/vitals_YYYYMMDD_HHMMSS.json |
Raw API response data for all apps, structured per metric |
data/vitals_YYYYMMDD_HHMMSS_com_example_app1.md |
AI-generated Markdown report for one app |
data/vitals_YYYYMMDD_HHMMSS_com_example_app2.md |
AI-generated Markdown report for another app |
data/reviews_YYYYMMDD_HHMMSS.json |
User reviews data (from --reviews-only mode) |
data/admob_YYYYMMDD_HHMMSS.json |
AdMob monetization data per app / ad unit (from fetch_admob.py) |
data/admob_YYYYMMDD_HHMMSS_com_example_app1.md |
AI-generated AdMob monetization report for one app |
{
"com.example.app1": {
"package_name": "com.example.app1",
"metrics": {
"anrRate": {
"rows": [
{
"startTime": {"year": 2026, "month": 5, "day": 5},
"metrics": [
{"decimalValue": {"value": "0.0023"}},
{"integerValue": {"value": "1500"}}
]
}
]
},
"crashRate": { ... },
"slowStartRate": {
"rows": [
{
"startTime": {"year": 2026, "month": 5, "day": 5},
"dimensions": [{"dimension": "startType", "stringValue": "COLD"}],
"metrics": [...]
}
]
},
"stuckBackgroundWakelockRate": { ... },
"excessiveWakeupRate": { ... },
"errorCounts": { ... }
},
"period": {
"start": "2026-05-03T00:00:00",
"end": "2026-05-06T00:00:00"
}
}
}| Metric | What it measures |
|---|---|
anrRate |
Application Not Responding rate per 100,000 users |
crashRate |
Crash rate per 100,000 users |
slowStartRate |
Slow start rate (broken down by COLD / WARM start type) |
stuckBackgroundWakelockRate |
Background wakelocks that exceed the allowed time |
excessiveWakeupRate |
Excessive wakeups (alarms, etc.) |
errorCounts |
Combined crash + ANR report count by type |
.
├── fetch_data.py # API fetcher: vitals + reviews, saves JSON
├── generate_report.py # LLM reporter: reads JSON, generates .md per app
├── fetch_admob.py # Standalone AdMob fetcher + LLM reporter (OAuth-based)
├── admob_oauth_setup.py # One-time helper to generate an AdMob refresh token
├── admob_ad_units.example.json # Template for declaring ad units per app
├── run_reports.sh # Bash orchestrator: fetch + report (vitals/reviews)
├── run_admob.sh # Bash wrapper for AdMob monitoring
├── weekly_report.sh # Convenience: 7-day report shortcut
├── examples/ # Sanitized sample outputs (reports + JSON structures)
├── .env # Your credentials and configuration (gitignored)
├── .env.example # Template for .env
├── service_account.json # Google service account key for vitals/reviews (gitignored)
├── admob_oauth_client.json # OAuth client JSON for AdMob (gitignored, optional)
├── requirements.txt # Python dependencies
├── data/ # All output files (gitignored)
└── README.md # This file
The current design is intentionally flat and single-purpose, focused on Google Play Vitals. As new data sources are added (e.g., user reviews, ratings, in-app review data, subscription insights), the architecture will be refactored into a more modular structure:
monitor/
core/
auth.py # Authenticate to any Google API
freshness.py # Shared freshness retry logic
storage.py # JSON file management
reporter.py # Generic LLM prompt builder
fetchers/
base.py # Abstract fetcher interface
vitals.py # Current vitals logic (extracted from fetch_data.py)
reviews.py # Future: user reviews / comments
ratings.py # Future: star ratings
config.py # Env + CLI handling
Key design principles for the refactored version:
- Each fetcher is independent — it registers itself, declares its own API scope, and produces its own output shape.
- The reporter is source-agnostic — each data source provides its own prompt builder, and the reporter only orchestrates the LLM call.
- Shared utilities are extracted — authentication, freshness retry, and file management are reused across all fetchers.
main.pydiscovers fetchers — no manual wiring when adding a new source.
If you're interested in contributing, the current fetch_data.py is the best place to study the patterns before the refactoring.
You need to enable the API in your Google Cloud project. See the Setup Guide above, step 1.2.
After enabling, it may take a few minutes to propagate.
The API has a processing delay (typically 3–5 days). The script handles this automatically by parsing the error and retrying with the correct date. This is normal — you'll see a log message like:
Adjusting end_date to freshness: 2026-05-07
Some metrics may not be available in your version of the API client library. The script only queries the metrics that are present. If a specific metric is missing from your API version, consider updating the client:
pip install --upgrade google-api-python-client- Make sure the service account has been granted access in the Play Console (see step 2).
- Verify the package name is correct.
- The app must have some user traffic — apps with no users will have no data.
Set GEMINI_API_KEY in your .env file. See section 3.2.
Released under the Apache License 2.0.