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" />
|
<meta name="theme-color" content="#3b82f6" />
|
||||||
<link rel="canonical" href="https://dzianisv.github.io/opencode-mobile/" />
|
<link rel="canonical" href="https://dzianisv.github.io/opencode-mobile/" />
|
||||||
<link rel="icon" type="image/png" href="fdroid/repo/icons/icon.png" />
|
<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 -->
|
<!-- Open Graph -->
|
||||||
<meta property="og:type" content="website" />
|
<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