From e07e592ef08da5fd310e98ff012ba5249fea18de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Tue, 6 Oct 2026 21:41:45 +0200 Subject: [PATCH 1/4] feat: 10-minute stats during the v0.14.0 launch window; exclude updater assets from totals - collect-stats runs every 10 min; a cadence step collects every time until WINDOW_END (2026-10-27), afterwards only when the last sample is ~6 h old. Concurrency group + rebase before push so runs don't race the Play workflow. - data/launch-events.csv: timestamped launch posts to line up with the samples. - Headline downloads exclude Tauri updater assets (latest.json polls, .sig, .app.tar.gz); they stay in stats-assets.csv as their own signal. --- .github/workflows/collect-stats.yml | 33 ++++++++++++++++++++++++++++- README.md | 3 +++ collect_stats.py | 16 +++++++++++++- data/launch-events.csv | 3 +++ test_collect_stats.py | 15 +++++++++++++ 5 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 data/launch-events.csv create mode 100644 test_collect_stats.py diff --git a/.github/workflows/collect-stats.yml b/.github/workflows/collect-stats.yml index 20ab7d7b..4375cbb7 100644 --- a/.github/workflows/collect-stats.yml +++ b/.github/workflows/collect-stats.yml @@ -10,7 +10,15 @@ on: #pull_requests: # branches: [ master ] schedule: - - cron: '0 */6 * * *' + # Every 10 minutes; the "Decide whether to collect" step below only collects + # every 6 hours outside a launch window. GitHub may delay scheduled runs, so + # every row carries its own timestamp. + - cron: '*/10 * * * *' + +# Never let two collections race on the push. +concurrency: + group: collect-stats + cancel-in-progress: false jobs: build: @@ -21,13 +29,32 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Decide whether to collect + id: cadence + run: | + # High-resolution window (UTC) around the v0.14.0 launch; 10-minute + # samples correlate downloads with posts (data/launch-events.csv). + # Outside it, collect only if the last sample is ~6 h old. + WINDOW_END=2026-10-27 + now=$(date -u +%s) + last=$(tail -n 1 data/stats.csv | cut -d, -f1) + age=$(( now - $(date -u -d "$last" +%s) )) + if [ "$GITHUB_EVENT_NAME" != "schedule" ] || [ "$now" -lt "$(date -u -d "$WINDOW_END" +%s)" ] || [ "$age" -ge 21000 ]; then + echo "collect=true" >> "$GITHUB_OUTPUT" + else + echo "collect=false" >> "$GITHUB_OUTPUT" + echo "Outside the launch window and last sample is only ${age}s old; skipping." + fi - name: Install uv + if: steps.cadence.outputs.collect == 'true' uses: astral-sh/setup-uv@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies + if: steps.cadence.outputs.collect == 'true' run: uv sync - name: Collect and append stats + if: steps.cadence.outputs.collect == 'true' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -42,12 +69,16 @@ jobs: # breakdowns). Appends only assets whose count changed since last logged. uv run python3 collect_stats.py --assets --csv >> data/stats-assets.csv - name: Commit + if: steps.cadence.outputs.collect == 'true' run: | git config --local user.email "noreply@github.com" git config --local user.name "GitHub Action" git add data/stats.csv data/releases.csv data/stats-assets.csv git commit -m "chore: update stats" + # The Play Console workflow also pushes to master; rebase onto it. + git pull --rebase origin master - name: Push changes + if: steps.cadence.outputs.collect == 'true' uses: ad-m/github-push-action@fe38f0a751bf9149f0270cc1fe20bf9156854365 with: github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index cdcf764d..7040dfa2 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,9 @@ The following is manually updated: - `chrome-weekly-users.csv` - Chrome extension weekly active users - `firefox-daily-users.csv` - Firefox extension daily active users - `notes.csv` - Manual entries of major/interesting events + - `launch-events.csv` - Timestamped launch posts (UTC; channel, url, note), to line up with the + high-resolution download samples. During a launch window (see `WINDOW_END` in + `.github/workflows/collect-stats.yml`) stats are collected every ~10 minutes instead of every 6 hours. ## Android vitals diff --git a/collect_stats.py b/collect_stats.py index 00ac54ec..f51e2cc3 100644 --- a/collect_stats.py +++ b/collect_stats.py @@ -78,10 +78,24 @@ def downloads_by_asset() -> list[dict]: return rows +def is_updater_asset(asset_name: str) -> bool: + """Assets fetched by the Tauri in-app updater, not by people downloading. + + `latest.json` is polled by every running aw-tauri install (via + releases/latest/download/latest.json), and the `.sig` / `.app.tar.gz` files + are what the updater downloads to apply an update. Their counts measure + update checks and applied updates, so they are kept in the per-asset series + but excluded from the headline download total. + """ + return asset_name == "latest.json" or asset_name.endswith((".sig", ".app.tar.gz")) + + def downloads(verbose=False) -> int: - """Total downloads across all release assets.""" + """Total downloads across all release assets, excluding updater traffic.""" total = 0 for r in downloads_by_asset(): + if is_updater_asset(r["asset"]): + continue if verbose: print(f' - {r["tag"]} [{r["platform"]}] {r["asset"]}: {r["downloads"]}') total += r["downloads"] diff --git a/data/launch-events.csv b/data/launch-events.csv new file mode 100644 index 00000000..32c733be --- /dev/null +++ b/data/launch-events.csv @@ -0,0 +1,3 @@ +timestamp,channel,url,note +2026-10-06T13:38:00+00:00,github,https://github.com/ActivityWatch/activitywatch/releases/tag/v0.14.0,v0.14.0 published +2026-10-06T17:00:00+00:00,discord,,v0.14.0 announcement in #announcements (approximate time) diff --git a/test_collect_stats.py b/test_collect_stats.py new file mode 100644 index 00000000..d18c5273 --- /dev/null +++ b/test_collect_stats.py @@ -0,0 +1,15 @@ +from collect_stats import is_updater_asset, platform + + +def test_updater_assets_are_excluded_from_downloads(): + assert is_updater_asset("latest.json") + assert is_updater_asset("activitywatch-tauri-0.14.0-darwin-aarch64.app.tar.gz") + assert is_updater_asset("activitywatch-tauri-0.14.0-linux-aarch64.AppImage.sig") + assert not is_updater_asset("activitywatch-v0.14.0-windows-x86_64-setup.exe") + assert not is_updater_asset("activitywatch-tauri-0.14.0-linux-x86_64.AppImage") + assert not is_updater_asset("activitywatch-v0.14.0-macos-arm64.dmg") + + +def test_platform(): + assert platform("activitywatch-tauri-0.14.0-darwin-aarch64.app.tar.gz") == "macos" + assert platform("latest.json") == "unknown" From f67fa285db626c1d96a446eff7170c3a152d1c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Tue, 6 Oct 2026 21:42:53 +0200 Subject: [PATCH 2/4] feat(analysis): drop updater assets from per-platform/version series; add updater_checks() --- analyze_stats.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/analyze_stats.py b/analyze_stats.py index 8292fba9..7db7f3a2 100644 --- a/analyze_stats.py +++ b/analyze_stats.py @@ -4,6 +4,8 @@ import matplotlib.pyplot as plt import pandas as pd +from collect_stats import is_updater_asset + def _load_downloads(): df = pd.read_csv("data/stats.csv", index_col="timestamp", parse_dates=True) @@ -50,18 +52,35 @@ def _load_android_rating(): return df -def _load_assets(): +def _load_assets(include_updater: bool = False): """Raw per-asset download counts over time (long format). Columns: timestamp, tag, asset, platform, downloads. The collector logs an asset only when its count changes, so this is sparse — see _asset_series() for the forward-filled reconstruction. + + Tauri updater assets (latest.json polls, .sig, .app.tar.gz) are dropped + unless `include_updater`: they count update checks, not downloads. See + updater_checks(). """ df = pd.read_csv("data/stats-assets.csv") df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True) + if not include_updater: + df = df[~df["asset"].map(is_updater_asset)] return df +def updater_checks(df=None): + """Time series of Tauri update checks (latest.json fetches) per release. + + Every running aw-tauri polls releases/latest/download/latest.json, so the + growth rate of the current release's count tracks active Tauri installs. + """ + df = _load_assets(include_updater=True) if df is None else df + df = df[df["asset"] == "latest.json"] + return _asset_series(df) + + def _asset_series(df=None): """Wide per-(tag, asset) download counts, forward-filled over time. From 07c0318702a0fff5d9cfe9e7042ae71ea20fb449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Tue, 6 Oct 2026 21:48:22 +0200 Subject: [PATCH 3/4] fix: rebase the Play workflow's push too; test the download total --- .github/workflows/collect-play.yml | 2 ++ test_collect_stats.py | 38 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/.github/workflows/collect-play.yml b/.github/workflows/collect-play.yml index 5770a488..2e834ba3 100644 --- a/.github/workflows/collect-play.yml +++ b/.github/workflows/collect-play.yml @@ -58,6 +58,8 @@ jobs: # android-tracks.csv may not exist yet if play_tracks.py lacked permission [ -f data/android-tracks.csv ] && git add data/android-tracks.csv || true git diff --cached --quiet || git commit -m "chore: update Play Console stats" + # collect-stats pushes every ~10 min during launch windows; rebase onto it. + git pull --rebase origin master - name: Push changes if: env.HAS_SA == 'true' uses: ad-m/github-push-action@fe38f0a751bf9149f0270cc1fe20bf9156854365 diff --git a/test_collect_stats.py b/test_collect_stats.py index d18c5273..f4dfe93b 100644 --- a/test_collect_stats.py +++ b/test_collect_stats.py @@ -1,3 +1,4 @@ +import collect_stats from collect_stats import is_updater_asset, platform @@ -13,3 +14,40 @@ def test_updater_assets_are_excluded_from_downloads(): def test_platform(): assert platform("activitywatch-tauri-0.14.0-darwin-aarch64.app.tar.gz") == "macos" assert platform("latest.json") == "unknown" + + +def test_downloads_total_excludes_updater_assets(monkeypatch): + assets = [ + { + "tag": "v0.14.0", + "asset": "activitywatch-v0.14.0-windows-x86_64-setup.exe", + "platform": "windows", + "downloads": 300, + }, + { + "tag": "v0.14.0", + "asset": "activitywatch-tauri-0.14.0-linux-x86_64.AppImage", + "platform": "linux", + "downloads": 20, + }, + { + "tag": "v0.14.0", + "asset": "latest.json", + "platform": "unknown", + "downloads": 51, + }, + { + "tag": "v0.14.0", + "asset": "activitywatch-tauri-0.14.0-darwin-aarch64.app.tar.gz", + "platform": "macos", + "downloads": 6, + }, + { + "tag": "v0.14.0", + "asset": "activitywatch-tauri-0.14.0-darwin-aarch64.app.tar.gz.sig", + "platform": "macos", + "downloads": 3, + }, + ] + monkeypatch.setattr(collect_stats, "downloads_by_asset", lambda: assets) + assert collect_stats.downloads() == 320 From 2d9addaf5204ba96a52ff71573ad3ba7e8614dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Tue, 6 Oct 2026 21:54:59 +0200 Subject: [PATCH 4/4] fix: push with rebase-and-retry in both stats workflows --- .github/workflows/collect-play.yml | 13 ++++++++----- .github/workflows/collect-stats.yml | 13 ++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.github/workflows/collect-play.yml b/.github/workflows/collect-play.yml index 2e834ba3..4d73fa45 100644 --- a/.github/workflows/collect-play.yml +++ b/.github/workflows/collect-play.yml @@ -58,10 +58,13 @@ jobs: # android-tracks.csv may not exist yet if play_tracks.py lacked permission [ -f data/android-tracks.csv ] && git add data/android-tracks.csv || true git diff --cached --quiet || git commit -m "chore: update Play Console stats" - # collect-stats pushes every ~10 min during launch windows; rebase onto it. - git pull --rebase origin master - name: Push changes if: env.HAS_SA == 'true' - uses: ad-m/github-push-action@fe38f0a751bf9149f0270cc1fe20bf9156854365 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} + run: | + # Both stats workflows push to master; retry on a rejected push. + for i in 1 2 3 4 5; do + git push origin HEAD:master && exit 0 + sleep $((i * 5)) + git pull --rebase origin master + done + exit 1 diff --git a/.github/workflows/collect-stats.yml b/.github/workflows/collect-stats.yml index 4375cbb7..60e626a9 100644 --- a/.github/workflows/collect-stats.yml +++ b/.github/workflows/collect-stats.yml @@ -75,10 +75,13 @@ jobs: git config --local user.name "GitHub Action" git add data/stats.csv data/releases.csv data/stats-assets.csv git commit -m "chore: update stats" - # The Play Console workflow also pushes to master; rebase onto it. - git pull --rebase origin master - name: Push changes if: steps.cadence.outputs.collect == 'true' - uses: ad-m/github-push-action@fe38f0a751bf9149f0270cc1fe20bf9156854365 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} + run: | + # Both stats workflows push to master; retry on a rejected push. + for i in 1 2 3 4 5; do + git push origin HEAD:master && exit 0 + sleep $((i * 5)) + git pull --rebase origin master + done + exit 1