From 53bf37a8b281bd03a0aae8534680a95fc2c980e6 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 14 Apr 2026 11:16:11 +0100 Subject: [PATCH] Add windowed telemetry adoption snapshots --- .../internal/subsystems/security-privacy.md | 5 + scripts/telemetry_adoption_report.py | 148 ++++++++++-------- .../tests/test_telemetry_adoption_report.py | 58 +++++++ 3 files changed, 149 insertions(+), 62 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index 094f27ca3..816214038 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -96,6 +96,11 @@ evidence with no subsystem ownership. That same governed home now also owns the single "usage data" vocabulary for anonymous outbound telemetry and local-only upgrade events, so Pulse stops describing those two privacy scopes as unrelated systems. +That same operator-reporting boundary now also owns reusable latest-install +adoption baselines. `scripts/telemetry_adoption_report.py` must emit +windowed 24h, 72h, and 7d latest-install snapshots that split published +versions from unpublished or development builds, so RC adoption reads stop +depending on ad hoc SQL or one-off local helper scripts. Security-facing settings remain intentionally shared with `frontend-primitives` because shell framing and presentation consistency still belong there, but the diff --git a/scripts/telemetry_adoption_report.py b/scripts/telemetry_adoption_report.py index 4119d9841..f4a8c3bb3 100644 --- a/scripts/telemetry_adoption_report.py +++ b/scripts/telemetry_adoption_report.py @@ -23,6 +23,11 @@ from urllib.request import Request, urlopen DEFAULT_DB_PATH = "/var/lib/pulse-license/licenses.sqlite" DEFAULT_GITHUB_REPO = "rcourtman/Pulse" +DEFAULT_LATEST_INSTALL_WINDOWS = ( + ("24h", timedelta(hours=24)), + ("72h", timedelta(hours=72)), + ("7d", timedelta(days=7)), +) GIT_DESCRIBE_RE = re.compile( r"^(?P\d+\.\d+\.\d+(?:-[0-9A-Za-z\.-]+)?)-(?P\d+)-g(?P[0-9a-fA-F]+)(?P-dirty)?$" ) @@ -213,6 +218,51 @@ finally: return json.loads(result.stdout) +def counter_entries(counter: Counter[str], key_name: str) -> list[dict[str, Any]]: + return [ + {key_name: value, "installs": installs} + for value, installs in sorted(counter.items(), key=lambda item: (-item[1], item[0])) + ] + + +def summarize_latest_install_windows( + latest_by_install: dict[str, dict[str, Any]], + published_versions: set[str], + *, + now: datetime | None = None, + windows: tuple[tuple[str, timedelta], ...] = DEFAULT_LATEST_INSTALL_WINDOWS, +) -> dict[str, Any]: + current_time = now or datetime.now(timezone.utc) + summary: dict[str, Any] = {} + + for label, limit in windows: + version_split: Counter[str] = Counter() + published_split: Counter[str] = Counter() + non_release_split: Counter[str] = Counter() + platform_split: Counter[str] = Counter() + + for row in latest_by_install.values(): + received_at = parse_received_at(str(row["received_at"])) + if current_time - received_at > limit: + continue + platform = str(row.get("platform") or "unknown").strip() or "unknown" + identity = classify_reported_version(str(row.get("version") or ""), published_versions) + version_split[identity.version] += 1 + platform_split[platform] += 1 + target = published_split if identity.is_published_release else non_release_split + target[identity.version] += 1 + + summary[label] = { + "active_installs": sum(version_split.values()), + "latest_versions": counter_entries(version_split, "version"), + "published_versions": counter_entries(published_split, "version"), + "non_release_versions": counter_entries(non_release_split, "version"), + "platforms": counter_entries(platform_split, "platform"), + } + + return summary + + def summarize_rows( db_stats: dict[str, Any], rows: Iterable[dict[str, Any]], @@ -225,50 +275,20 @@ def summarize_rows( if existing is None or str(row["received_at"]) > str(existing["received_at"]): latest_by_install[install_id] = row - now = datetime.now(timezone.utc) - active_24h = 0 - active_72h = 0 - platform_split = Counter() - version_split = Counter() - published_split = Counter() - non_release_split = Counter() - - for row in latest_by_install.values(): - received_at = parse_received_at(str(row["received_at"])) - age = now - received_at - if age <= timedelta(hours=24): - active_24h += 1 - if age <= timedelta(hours=72): - active_72h += 1 - platform = str(row.get("platform") or "unknown").strip() or "unknown" - platform_split[platform] += 1 - identity = classify_reported_version(str(row.get("version") or ""), published_versions) - version_split[identity.version] += 1 - target = published_split if identity.is_published_release else non_release_split - target[identity.version] += 1 + latest_install_windows = summarize_latest_install_windows(latest_by_install, published_versions) + summary_72h = latest_install_windows["72h"] return { "db_stats": db_stats, + "latest_install_windows": latest_install_windows, "active_latest": { - "active_24h": active_24h, - "active_72h": active_72h, + "active_24h": latest_install_windows["24h"]["active_installs"], + "active_72h": summary_72h["active_installs"], }, - "latest_version_split_72h": [ - {"version": version, "installs": installs} - for version, installs in sorted(version_split.items(), key=lambda item: (-item[1], item[0])) - ], - "published_version_split_72h": [ - {"version": version, "installs": installs} - for version, installs in sorted(published_split.items(), key=lambda item: (-item[1], item[0])) - ], - "non_release_version_split_72h": [ - {"version": version, "installs": installs} - for version, installs in sorted(non_release_split.items(), key=lambda item: (-item[1], item[0])) - ], - "latest_platform_split_72h": [ - {"platform": platform, "installs": installs} - for platform, installs in sorted(platform_split.items(), key=lambda item: (-item[1], item[0])) - ], + "latest_version_split_72h": summary_72h["latest_versions"], + "published_version_split_72h": summary_72h["published_versions"], + "non_release_version_split_72h": summary_72h["non_release_versions"], + "latest_platform_split_72h": summary_72h["platforms"], } @@ -280,30 +300,34 @@ def format_text(summary: dict[str, Any], repo: str, since_days: int) -> str: f"latest ping: {summary['db_stats'].get('latest_ping') or 'unknown'}", f"total rows: {summary['db_stats'].get('total_rows', 0)}", f"total distinct installs: {summary['db_stats'].get('total_distinct_installs', 0)}", - f"active installs (24h): {summary['active_latest']['active_24h']}", - f"active installs (72h): {summary['active_latest']['active_72h']}", - "", - "Published versions (latest install state in last 72h):", ] - published = summary["published_version_split_72h"] - if published: - lines.extend(f"- {entry['version']}: {entry['installs']}" for entry in published) - else: - lines.append("- none") - lines.append("") - lines.append("Non-release or unpublished versions (latest install state in last 72h):") - non_release = summary["non_release_version_split_72h"] - if non_release: - lines.extend(f"- {entry['version']}: {entry['installs']}" for entry in non_release) - else: - lines.append("- none") - lines.append("") - lines.append("Platforms (latest install state in last 72h):") - platform_split = summary["latest_platform_split_72h"] - if platform_split: - lines.extend(f"- {entry['platform']}: {entry['installs']}" for entry in platform_split) - else: - lines.append("- none") + + for label, _ in DEFAULT_LATEST_INSTALL_WINDOWS: + window_summary = summary["latest_install_windows"][label] + lines.extend( + [ + "", + f"Latest install state ({label}):", + f"- active installs: {window_summary['active_installs']}", + "- published versions:", + ] + ) + if window_summary["published_versions"]: + lines.extend(f" - {entry['version']}: {entry['installs']}" for entry in window_summary["published_versions"]) + else: + lines.append(" - none") + lines.append("- non-release or unpublished versions:") + if window_summary["non_release_versions"]: + lines.extend( + f" - {entry['version']}: {entry['installs']}" for entry in window_summary["non_release_versions"] + ) + else: + lines.append(" - none") + lines.append("- platforms:") + if window_summary["platforms"]: + lines.extend(f" - {entry['platform']}: {entry['installs']}" for entry in window_summary["platforms"]) + else: + lines.append(" - none") return "\n".join(lines) diff --git a/scripts/tests/test_telemetry_adoption_report.py b/scripts/tests/test_telemetry_adoption_report.py index 091c4e515..9cafcb61c 100644 --- a/scripts/tests/test_telemetry_adoption_report.py +++ b/scripts/tests/test_telemetry_adoption_report.py @@ -79,6 +79,9 @@ class TelemetryAdoptionReportTest(unittest.TestCase): self.assertEqual(summary["active_latest"]["active_24h"], 2) self.assertEqual(summary["active_latest"]["active_72h"], 3) + self.assertEqual(summary["latest_install_windows"]["24h"]["active_installs"], 2) + self.assertEqual(summary["latest_install_windows"]["72h"]["active_installs"], 3) + self.assertEqual(summary["latest_install_windows"]["7d"]["active_installs"], 3) self.assertEqual( summary["published_version_split_72h"], [{"version": "6.0.0-rc.1", "installs": 1}], @@ -90,6 +93,61 @@ class TelemetryAdoptionReportTest(unittest.TestCase): {"version": "6.0.0-rc.2", "installs": 1}, ], ) + self.assertEqual( + summary["latest_install_windows"]["24h"]["published_versions"], + [{"version": "6.0.0-rc.1", "installs": 1}], + ) + self.assertEqual( + summary["latest_install_windows"]["72h"]["non_release_versions"], + [ + {"version": "0.0.0-feature-new-metric", "installs": 1}, + {"version": "6.0.0-rc.2", "installs": 1}, + ], + ) + self.assertEqual( + summary["latest_install_windows"]["7d"]["platforms"], + [ + {"platform": "binary", "installs": 2}, + {"platform": "docker", "installs": 1}, + ], + ) + + def test_format_text_includes_latest_install_windows(self) -> None: + summary = { + "db_stats": { + "latest_ping": "2026-04-14 10:04:08", + "total_rows": 3228, + "total_distinct_installs": 229, + }, + "latest_install_windows": { + "24h": { + "active_installs": 127, + "published_versions": [{"version": "6.0.0-rc.1", "installs": 91}], + "non_release_versions": [{"version": "6.0.0-rc.2", "installs": 32}], + "platforms": [{"platform": "docker", "installs": 66}], + }, + "72h": { + "active_installs": 153, + "published_versions": [{"version": "6.0.0-rc.1", "installs": 117}], + "non_release_versions": [{"version": "6.0.0-rc.2", "installs": 32}], + "platforms": [{"platform": "binary", "installs": 83}], + }, + "7d": { + "active_installs": 157, + "published_versions": [{"version": "6.0.0-rc.1", "installs": 118}], + "non_release_versions": [{"version": "6.0.0-rc.2", "installs": 32}], + "platforms": [{"platform": "binary", "installs": 87}], + }, + }, + } + + rendered = report.format_text(summary, "rcourtman/Pulse", 7) + + self.assertIn("Latest install state (24h):", rendered) + self.assertIn("Latest install state (72h):", rendered) + self.assertIn("Latest install state (7d):", rendered) + self.assertIn(" - 6.0.0-rc.1: 118", rendered) + self.assertIn(" - 6.0.0-rc.2: 32", rendered) if __name__ == "__main__":