Skip to content

milouk/play-analytics

Repository files navigation

play-analytics

a self-hosted dashboard for Google Play Console data — installs, retention, crashes, sales, reviews, all in one HTML page

build ghcr last commit license ko-fi

🛰️ Live demo: milouk.me/projects/play-analytics

Pulls every metric Google Play exposes for your apps — daily install reports, crash & ANR counts, ratings, store-listing visitors, monthly sales & earnings CSVs, in-app product catalogs, and live reviews — then renders a self-contained dark-mode HTML dashboard for each app and a top-level index that links to all of them. One developer account, any number of apps, one image, configured entirely with env vars.

Quickstart

docker run --rm \
  -e DEVELOPER_ACCOUNT_ID=1234567890123456789 \
  -e APPS="com.example.app1:My App,com.example.app2:Other App" \
  -e GOOGLE_APPLICATION_CREDENTIALS_JSON="$(cat key.json)" \
  -v "$(pwd)/output:/app/output" \
  ghcr.io/milouk/play-analytics:latest
open output/index.html

Or with the built-in HTTP server:

docker run --rm -p 8080:8080 \
  -e DEVELOPER_ACCOUNT_ID=... -e APPS=... \
  -e GOOGLE_APPLICATION_CREDENTIALS_JSON="$(cat key.json)" \
  -e SERVE=true \
  ghcr.io/milouk/play-analytics:latest
# → http://localhost:8080/

What you get

For each app in $APPS:

  • output/<package>/dashboard.html — single-file HTML, dark theme, Chart.js
  • output/<package>/report.md — Markdown summary (PR/Slack-friendly)

Plus:

  • output/index.html — landing page with a card per app

Each dashboard surfaces:

Section Metrics
Headline KPIs active devices today · user installs · net retention · paid orders · paid conversion · net earnings · avg rating · crashes / ANRs
Growth daily user installs (bars) overlaid with active devices (line)
Geography & audience installs by country (top 15) · language · device · Android OS · app version
Quality daily crashes & ANRs · star distribution from review CSVs
Monetisation every charged sales transaction · revenue by currency · paying countries
Voice of customer full review text with stars, device, version, developer reply
Catalog live in-app product list + subscription list + release tracks
Pricing recommendations per-country PPP-tier diagnostic for every IAP — cut / hold / raise / fine verdicts with one-click apply buttons that copy a CLI command to write the change to Play

What it reads

Two sources, both via the service-account key:

  1. gs://pubsite_prod_<developer_account_id>/ — the GCS bucket where Play writes your daily install/crash/rating CSVs and monthly sales/earnings zips. Folders: stats/installs/, stats/crashes/, stats/ratings/, stats/store_performance/, reviews/, sales/, earnings/.
  2. Android Publisher API v3 — live catalog (monetization.onetimeproducts, monetization.subscriptions), the trailing ~7-day review window (reviews.list), and current release tracks + localised listings.

Downloads are incremental (skips files whose local size matches the blob), so a re-run after a fresh report drop is fast.

One-time setup

You need three things: a service account, the developer account ID, and read access for that service account on the reports bucket.

1. Service account

In Google Cloud Console → IAM → Service Accounts → Create. No GCP roles needed; the permissions live in Play Console and on the bucket. Download the JSON key — this is the file you pass via GOOGLE_APPLICATION_CREDENTIALS_JSON.

2. Grant the service account access in Play Console

Play Console → Users and permissions → Invite new user → use the service account email. Grant at minimum:

  • App permissions: View app information and download bulk reports (for every app in $APPS)
  • Account permissions (optional, enables live catalog/listings): View store listings, View financial data

3. Grant bucket read access

The reports bucket lives in a Google-managed project, not yours. The service account needs to be added as a reader:

gsutil iam ch \
  serviceAccount:play-analytics@your-project.iam.gserviceaccount.com:roles/storage.objectViewer \
  gs://pubsite_prod_<your-developer-account-id>

Run this once from a Google account that has Owner on the dev account.

4. Find your developer account ID

Play Console → Settings → Developer account → Account details → Account ID. It's a 19-digit number.

5. Enumerate your apps

Use the package name from Play Console — app.foo.bar style. Pretty names are optional but recommended.

APPS=app.acme.notes:Acme Notes,app.acme.timer:Acme Timer

Configuration

All via env vars. See .env.example for the full list.

Variable Required Purpose
DEVELOPER_ACCOUNT_ID yes 19-digit ID from Play Console
APPS yes comma-separated package[:Pretty Name] list
GOOGLE_APPLICATION_CREDENTIALS_JSON one of JSON content as a string (env-friendly)
GOOGLE_APPLICATION_CREDENTIALS one of filesystem path to the JSON key
DATA_DIR no cache for downloaded reports — default /app/data
OUTPUT_DIR no where dashboards go — default /app/output
SERVE no true to serve OUTPUT_DIR over HTTP after rendering
PORT no server port if SERVE=true — default 8080
WINDOWS no comma-separated time windows to render — default 7,30,all. Each becomes a tab in the dashboard. Use 7d,14d,30d,90d,all, etc.
CRON_SCHEDULE no standard 5-field cron expression. When set, the image runs main.py on schedule (via supercronic) — typical: 0 6 * * * for daily 06:00 UTC refresh

Running it

Docker (recommended)

docker run --rm \
  -e DEVELOPER_ACCOUNT_ID=... \
  -e APPS="pkg.a:App A,pkg.b:App B" \
  -e GOOGLE_APPLICATION_CREDENTIALS_JSON="$(cat key.json)" \
  -v "$(pwd)/data:/app/data" \
  -v "$(pwd)/output:/app/output" \
  ghcr.io/milouk/play-analytics:latest

Mounting data/ is optional but makes re-runs near-instant (it's used as a cache).

Docker Compose (set-and-forget self-host)

cp .env.example .env
# edit .env
docker compose up -d

The compose file enables SERVE=true and exposes port 8080.

GitHub Actions (scheduled, no server)

name: refresh dashboards
on:
  schedule: [{ cron: "0 6 * * *" }]
  workflow_dispatch:

jobs:
  refresh:
    runs-on: ubuntu-latest
    permissions: { contents: write }
    steps:
      - uses: actions/checkout@v4
      - run: |
          docker run --rm \
            -e DEVELOPER_ACCOUNT_ID=${{ secrets.PLAY_DEV_ID }} \
            -e APPS=${{ vars.PLAY_APPS }} \
            -e GOOGLE_APPLICATION_CREDENTIALS_JSON='${{ secrets.GOOGLE_SA_JSON }}' \
            -v ${{ github.workspace }}/output:/app/output \
            ghcr.io/milouk/play-analytics:latest
      - uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./output

This publishes your dashboards to GitHub Pages on a schedule, no server required.

Locally with Python (no Docker)

python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
export DEVELOPER_ACCOUNT_ID=...
export APPS="pkg.a:App A"
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json
.venv/bin/python main.py            # fetch + render
.venv/bin/python main.py --no-fetch # rerender from cache
.venv/bin/python main.py --serve    # also start http.server on 8080

How it's built

  • Python 3.12, stdlib + four Google client libs (no pandas, no flask, no jinja)
  • google-cloud-storage for the reports bucket (drops the gcloud SDK dependency entirely)
  • google-api-python-client for the live Android Publisher API
  • HTML rendered from string templates with embedded Chart.js (single <script> tag from jsDelivr CDN); each dashboard is a self-contained file
  • ~10 MB final image, multi-arch (linux/amd64, linux/arm64)

Pricing recommendations (and writing prices back to Play)

Each in-app product's dashboard section computes a per-country PPP verdict based on install/buyer signal and your anchor price:

  • cut — over the tier target with no paying buyers
  • hold — over target and has buyers — touching it risks ARPU
  • raise — below target (rare, usually after a too-aggressive cut)
  • fine — already at the best achievable tier price

Each actionable row has an apply button that copies a ready-to-paste CLI command to your clipboard:

python apply_pricing.py <package> <productId> --only TR --apply

The CLI runs in dry-run mode without --apply — it fetches the current product, builds the planned diff, and prints it. With --apply it sends one atomic monetization.onetimeproducts.patch covering every change at once, then re-fetches every region to verify the new price took.

There's also an ⚡ apply all N button at the top of each pricing card that copies a single command for the entire actionable batch.

The script will never touch a region without explicit install signal, will never re-price a region marked hold (has buyers), and refuses to proceed unless the planned diff is exactly what you'd expect.

Caveats

  • Reports lag ~48 h. Today's data is never present.
  • Daily Device Uninstalls is unreliable in Google's reports (often all zero). This tool uses Uninstall events + Daily User Uninstalls for retention math; the dashboard shows real numbers.
  • The Publisher API's reviews.list returns only the trailing ~7 days. The monthly review CSVs in the bucket are the source of truth for full history.
  • Net earnings only appear once a month finalises (around the 15th of the following month).

License

MIT © Michael Loukeris

Acknowledgements

Pulls data exclusively from the Google Play Developer Reports and the Android Publisher API v3. No third-party APIs or telemetry.

About

Self-hosted dashboard for Google Play Console — installs, retention, crashes, sales, reviews per app. One developer account, any number of apps, configured via env vars. Multi-arch Docker image.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

Packages

 
 
 

Contributors