A self-hostable, single-binary tool that pulls your app's Google Play and App Store reviews into one place, tags them by type and theme (rules + optional LLM), detects trend spikes, and surfaces everything through a built-in dashboard, Google Chat alerts, and scheduled email digests.
One Go binary, an embedded SQLite database, and an embedded web UI. No external services required to run it.
- Features
- How it works
- Quick start
- Configuration
- Tagging rules
- CLI commands
- API
- Dashboard
- Querying the database directly
- Development
- Security notes
- Roadmap
- Contributing
- License
- Ingestion — pulls reviews from Google Play and the Apple App Store. Works out of the box via public scraping/RSS; upgrades automatically to official store APIs when you provide credentials.
- Tagging — every review gets a
type(bug / feature / issue / other) and zero-or-morethemes (payment, login, etc). A fast, editable rules engine runs first; an optional LLM classifier (Gemini) only runs on reviews the rules can't confidently resolve. - Human corrections — operators can override any tag from the dashboard.
Human tags are permanent ground truth:
--retagand automated tagging never overwrite them. - Trend detection — daily metrics are aggregated per type/theme, then compared against a rolling baseline using both a relative-rise threshold and a robust (median/MAD) z-score, so noisy baselines don't cause false alarms.
- Alerts — active spikes above a configurable severity post to Google Chat, with de-duplication so you're not paged twice for the same trend.
- Reports — a periodic HTML + CSV digest (top themes, biggest risers, low ratings) viewable in-app or emailed via SMTP.
- Dashboard — an embedded web UI for overview stats, trend/spike drill down, a filterable review list with inline tag correction, and report history. No build step, no separate frontend deploy.
- One artifact — everything above ships in a single Go binary with an embedded SQLite database (WAL mode) and embedded static assets.
┌─────────┐ ┌─────────┐ ┌──────────┐ ┌────────┐ ┌─────────┐
stores │ sync │ ───▶ │ tag │ ───▶ │ analyze │ ───▶ │ notify │ │ report │
└─────────┘ └─────────┘ └──────────┘ └────────┘ └─────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌────────────────────────────────────────────────────────────┐
│ SQLite (reviews.db) │
│ reviews · review_tags · metrics_daily · trends · reports │
└────────────────────────────────────────────────────────────┘
▲
│
┌────────────────────┐
│ dashboard + API │ (reads + human corrections)
└────────────────────┘
syncfetches new reviews since the last known review per store and upserts them (safe to re-run;--backfilldoes a one-off historical pull).tagruns the rules engine, falling back to the LLM only when rules don't resolve a concrete type. Tags are cached by review-text hash to avoid redundant LLM calls.analyzerebuildsmetrics_daily(using the effective tag per review — human tags win over LLM, which wins over rules) and re-evaluates every type/theme series for spikes, updatingtrends(active/resolved).notifysends Google Chat alerts for active trends that haven't been alerted yet.reportrenders an HTML/CSV digest and, if SMTP is configured, emails it.--pipelinerunssync → tag → analyze → notifyin one shot — a good fit for a periodic cron job.
cp config.toml.sample config.toml
cp rules.toml.sample rules.toml
go run . --install --config config.toml # create the database schema
go run . --backfill --config config.toml # first-time historical import
go run . --tag --config config.toml # tag everything backfilled
go run . --analyze --config config.toml # build metrics + detect trends
go run . --config config.toml
# open http://127.0.0.1:9000Nothing above requires an API key or credentials — the scraper-based
ingestion and rules-only tagging work with zero config beyond the app
identifiers in config.toml.
Everything lives in one config.toml (see config.toml.sample for the full
annotated reference). The main sections:
| Section | Purpose |
|---|---|
[server] |
Bind address — must be 127.0.0.1 or localhost (enforced at startup) |
[database] |
Path to the SQLite file |
[app] |
Google Play package name / App Store slug for the app being tracked |
[ingest] |
Backfill window, request pacing |
[google_play], [app_store] |
Optional official API credentials (falls back to scraping/RSS if empty) |
[tagging] |
Path to rules.toml, batch size |
[llm] |
Optional Gemini API key/model for LLM-assisted tagging |
[analytics] |
Spike detection thresholds (recent/baseline window, relative %, z-score, volume floor) |
[notify] |
Google Chat webhook + minimum severity to alert on |
[reports] |
Digest period, recipients, CSV attachment |
[smtp] |
Outgoing mail server for report emails |
rules.toml is a separate, hot-editable file (no restart needed before
--retag) — see Tagging rules.
rules.toml maps regex patterns over the review title+body to a type
and/or theme:
[[rule]]
name = "payment_terms"
pattern = "(?i)payment|upi|card declined|transaction failed|refund failed|money debited"
theme = "payment"
[[rule]]
name = "crash_terms"
pattern = "(?i)crash|force close|app closes|anr|keeps stopping"
type = "bug"Edit freely, then run --retag to re-classify existing reviews. Human
corrections are never touched by this process.
| Flag | Description |
|---|---|
--install |
Create the database schema and exit |
--upgrade |
Apply pending migrations and exit |
--sync |
Fetch new reviews since the last sync and exit |
--backfill |
One-off historical import and exit |
--tag |
Tag reviews that have no tags yet, and exit |
--retag |
Re-tag all reviews with rules/LLM (keeps human corrections), and exit |
--analyze |
Rebuild metrics_daily, detect trends, send pending alerts, and exit |
--notify |
Send Google Chat alerts for pending active trends, and exit |
--report |
Generate a digest report (and email it if SMTP is configured), and exit |
--pipeline |
Run sync → tag → analyze → notify in sequence, and exit |
| (no flag) | Start the HTTP server + dashboard |
Run any of these on a schedule (cron, systemd timer, CI schedule) for periodic operation — there is currently no built-in scheduler (see Roadmap).
The dashboard is a thin client over a small JSON API:
| Method | Path | Purpose |
|---|---|---|
GET |
/api/health |
Liveness check |
GET |
/api/overview |
Summary stats (volume, rating, active spikes) |
GET |
/api/reviews |
Filterable review list (store, type, theme, limit, offset) |
PUT |
/api/reviews/{id}/tags |
Set human-corrected type/themes for a review |
GET |
/api/labels |
Valid type/theme label taxonomy |
GET |
/api/metrics/daily |
Daily aggregates, optionally filtered by dimension |
GET |
/api/trends |
Detected trends, optionally filtered by status |
GET |
/api/reports |
Report history |
GET |
/api/reports/{id} |
A single report (add ?format=html to view rendered) |
The embedded UI (no build step, served via go:embed) has four pages:
- Overview — headline stats and currently active spikes.
- Trends — every detected trend with severity, % change, and z-score.
- Reviews — the filterable review list with a Correct action to fix wrong or ambiguous tags inline.
- Reports — history of generated digests, viewable in-browser.
The SQLite file is a first-class integration point — read it with any tool while the app keeps running (WAL mode keeps reads and writes from blocking each other):
sqlite3 reviews.db "SELECT COUNT(*) FROM reviews;"
sqlite3 reviews.db "SELECT dimension, label, COUNT(*) FROM review_tags GROUP BY 1, 2;"Prefer file:reviews.db?mode=ro from external tools for read-only, WAL-safe
access.
go test ./...
go vet ./...Before cutting a release, work through plans/BUILD-CHECKLIST.md in the
repo root, which includes a manual SMTP email send test, Google Chat alert
re-test, and a --pipeline smoke test.
- The HTTP server must bind to
127.0.0.1orlocalhost— any other[server].addressis rejected at startup. There is no authentication wired in yet (internal/authdefines the seam, butmain.godoes not enforce it), so loopback binding is the primary access control until auth or a trusted reverse proxy is added. PUT /api/reviews/{id}/tagsmutates tagging ground truth and should be treated as an admin-only action once auth exists.- Secrets (
config.toml, App Store.p8keys,rules.tomlif it contains anything sensitive) are gitignored — only commit the.sampletemplates.
- In-process scheduler so periodic runs don't need external cron.
- React/Vite dashboard (current UI is server-rendered vanilla JS/HTML/CSS).
- Authentication / SSO in front of the dashboard and API.
- Prebuilt release binaries and an optional
docker-compose.yml.
Issues and pull requests are welcome. Before implementing a non-trivial
change, please open an issue or discussion describing the problem first —
this keeps the type/theme taxonomy and analytics thresholds consistent
across contributions. Run go test ./... && go vet ./... before submitting.
Released under the MIT License. Copyright (c) 2026 redBus.