feat: add Vercel analytics to website + Play Store review triage script
- docs-site/index.html: add Vercel Analytics + Speed Insights CDN scripts (only fires on opencode.agentlabs.cc served via Vercel, not GitHub Pages) - scripts/triage-reviews.py: fetch recent Play Store reviews via Android Publisher API, create GitHub issues for ≤3★ reviews not yet tracked Run review triage manually on VM: DAYS_BACK=7 GOOGLE_SERVICE_ACCOUNT_JSON=... GH_TOKEN=... python3 scripts/triage-reviews.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -9,6 +9,9 @@
|
||||
<meta name="theme-color" content="#3b82f6" />
|
||||
<link rel="canonical" href="https://dzianisv.github.io/opencode-mobile/" />
|
||||
<link rel="icon" type="image/png" href="fdroid/repo/icons/icon.png" />
|
||||
<!-- Vercel Web Analytics — privacy-first, no cookies -->
|
||||
<script defer src="https://cdn.vercel-insights.com/v1/speed-insights/script.js"></script>
|
||||
<script defer src="https://cdn.vercel-insights.com/v1/script.js"></script>
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
155
scripts/triage-reviews.py
Normal file
155
scripts/triage-reviews.py
Normal file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Play Store review triage script.
|
||||
|
||||
Fetches recent Play Store reviews via the Android Publisher API and creates
|
||||
GitHub issues for low-rated or bug-report reviews so they enter the normal
|
||||
fix cycle without manual monitoring.
|
||||
|
||||
Required env vars:
|
||||
GOOGLE_SERVICE_ACCOUNT_JSON — full JSON key for the service account
|
||||
GH_TOKEN — GitHub token with issues:write scope
|
||||
PACKAGE_NAME — Android package (cc.agentlabs.opencode)
|
||||
DAYS_BACK — how many days back to look (default 7)
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
import google.auth
|
||||
from google.oauth2 import service_account
|
||||
from googleapiclient.discovery import build
|
||||
|
||||
REPO = os.environ.get("GITHUB_REPOSITORY", "dzianisv/opencode-mobile")
|
||||
PACKAGE_NAME = os.environ.get("PACKAGE_NAME", "cc.agentlabs.opencode")
|
||||
DAYS_BACK = int(os.environ.get("DAYS_BACK", "7"))
|
||||
GH_TOKEN = os.environ.get("GH_TOKEN", "")
|
||||
|
||||
SCOPES = ["https://www.googleapis.com/auth/androidpublisher"]
|
||||
|
||||
|
||||
def get_service():
|
||||
raw = os.environ.get("GOOGLE_SERVICE_ACCOUNT_JSON", "")
|
||||
if not raw:
|
||||
print("GOOGLE_SERVICE_ACCOUNT_JSON not set — skipping.")
|
||||
sys.exit(0)
|
||||
info = json.loads(raw)
|
||||
creds = service_account.Credentials.from_service_account_info(info, scopes=SCOPES)
|
||||
return build("androidpublisher", "v3", credentials=creds, cache_discovery=False)
|
||||
|
||||
|
||||
def fetch_reviews(service):
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=DAYS_BACK)
|
||||
results = []
|
||||
token = None
|
||||
while True:
|
||||
resp = service.reviews().list(
|
||||
packageName=PACKAGE_NAME,
|
||||
maxResults=100,
|
||||
**({"token": token} if token else {}),
|
||||
).execute()
|
||||
for review in resp.get("reviews", []):
|
||||
comment = review.get("comments", [{}])[0].get("userComment", {})
|
||||
ts = int(comment.get("lastModified", {}).get("seconds", 0))
|
||||
if not ts:
|
||||
continue
|
||||
dt = datetime.fromtimestamp(ts, tz=timezone.utc)
|
||||
if dt < cutoff:
|
||||
continue
|
||||
results.append({
|
||||
"review_id": review.get("reviewId", ""),
|
||||
"author": review.get("authorName", "anonymous"),
|
||||
"rating": comment.get("starRating", 0),
|
||||
"text": comment.get("text", ""),
|
||||
"date": dt.strftime("%Y-%m-%d"),
|
||||
"lang": comment.get("reviewerLanguage", "en"),
|
||||
})
|
||||
token = resp.get("tokenPagination", {}).get("nextPageToken")
|
||||
if not token:
|
||||
break
|
||||
return results
|
||||
|
||||
|
||||
def issue_exists(title_prefix: str) -> bool:
|
||||
"""Check if a GitHub issue with this title prefix already exists."""
|
||||
result = subprocess.run(
|
||||
[
|
||||
"gh", "issue", "list",
|
||||
"--repo", REPO,
|
||||
"--search", title_prefix,
|
||||
"--state", "open",
|
||||
"--json", "title",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={**os.environ, "GH_TOKEN": GH_TOKEN},
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return False
|
||||
issues = json.loads(result.stdout or "[]")
|
||||
return any(title_prefix.lower() in i["title"].lower() for i in issues)
|
||||
|
||||
|
||||
def create_issue(review: dict):
|
||||
stars = "⭐" * review["rating"]
|
||||
title = f"[Play Store Review] {review['rating']}★ from {review['author']} on {review['date']}"
|
||||
body = f"""**Rating**: {stars} ({review['rating']}/5)
|
||||
**Author**: {review['author']}
|
||||
**Date**: {review['date']}
|
||||
**Language**: {review['lang']}
|
||||
**Review ID**: `{review['review_id']}`
|
||||
|
||||
---
|
||||
|
||||
> {review['text']}
|
||||
|
||||
---
|
||||
|
||||
*This issue was automatically created by the [monitor-reviews workflow](/.github/workflows/monitor-reviews.yml).
|
||||
Reply to the user in Play Console if applicable.*
|
||||
|
||||
**Labels**: `user-feedback`, `play-store-review`
|
||||
"""
|
||||
subprocess.run(
|
||||
[
|
||||
"gh", "issue", "create",
|
||||
"--repo", REPO,
|
||||
"--title", title,
|
||||
"--body", body,
|
||||
"--label", "user-feedback",
|
||||
],
|
||||
check=True,
|
||||
env={**os.environ, "GH_TOKEN": GH_TOKEN},
|
||||
)
|
||||
print(f" ✅ Created issue: {title}")
|
||||
|
||||
|
||||
def main():
|
||||
print(f"Fetching Play Store reviews for {PACKAGE_NAME} (last {DAYS_BACK} days)...")
|
||||
service = get_service()
|
||||
reviews = fetch_reviews(service)
|
||||
print(f"Found {len(reviews)} review(s) in window.")
|
||||
|
||||
# Triage: create issues for 1-3 star reviews (potential bugs/problems)
|
||||
actionable = [r for r in reviews if r["rating"] <= 3]
|
||||
print(f"Actionable (≤3★): {len(actionable)}")
|
||||
|
||||
for review in actionable:
|
||||
title_prefix = f"Play Store Review] {review['rating']}★ from {review['author']} on {review['date']}"
|
||||
if issue_exists(title_prefix):
|
||||
print(f" ⏭ Already exists: {review['author']} {review['date']}")
|
||||
continue
|
||||
create_issue(review)
|
||||
time.sleep(1) # avoid GitHub API rate limit
|
||||
|
||||
# Summary
|
||||
avg = sum(r["rating"] for r in reviews) / len(reviews) if reviews else 0
|
||||
print(f"\nSummary: {len(reviews)} reviews, avg rating {avg:.1f}★, {len(actionable)} issues created/checked.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user