From 9fdbc34900d9eecc59693ac4bf8632cd8efe69df Mon Sep 17 00:00:00 2001 From: Michael Neeley Date: Fri, 25 Sep 2026 20:51:47 -0400 Subject: [PATCH 1/8] Clarify app version sources and runtime freshness --- README.md | 18 +++++---- app_versions.py | 46 ++++++++++++++++------ templates/app_versions.html | 56 +++++++++++++------------- tests/test_app_versions.py | 78 ++++++++++++++++++++++++------------- 4 files changed, 123 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index e04e3df..0680376 100644 --- a/README.md +++ b/README.md @@ -209,18 +209,20 @@ The legacy `GET /airflow-fleet-health` Better Stack monitor endpoint has been re version signal per church/app/platform. It uses the analytics metadata sent by the mobile and TV apps, including the exported `apollos_version`, `app_version`, `app_update_id`, `bundle_id`, `application_name`, `church`, `apollos_platform`, `source_revision`, and `source_version` fields. -Public App Store versions are also looked up by bundle ID when available so iOS rows can distinguish -the observed installed app version from the current production App Store version. Roku Segment -exports currently do not expose `apollos_version`, so Roku rows use the exported +The public US Apple lookup is shown separately for iOS bundle IDs, with the fetch time. It can +lag App Store Connect and must not be treated as an authoritative published release. The seen +build is the version reported by the selected installation in Segment, not the latest shipped +build or a measure of all installations. Roku Segment exports currently do not expose +`apollos_version`, so Roku rows use the exported `context_library_version` and are labelled as analytics library versions. The page first inspects `INFORMATION_SCHEMA.COLUMNS` for the configured Segment tables and only queries tables that expose a supported version signal, so Segment lifecycle-only app-store -`version` fields are not mistaken for Apollos runtime versions. Runtime rows are marked outdated -when their highest observed runtime is behind the highest runtime observed for the same platform in -the lookback window, or when the observed app version is behind an available App Store version. If -older clients are still active after a release, the dashboard keeps the highest observed runtime for -the app instead of letting the most recent older-client event hide it. Mobile rows prefer the +`version` fields are not mistaken for Apollos runtime versions. Source freshness means *behind +top seen* or *top seen* within the same platform, not behind an App Store release. Missing or +uncomparable signals are *unverified*. If older clients are still active after a release, the +dashboard keeps the highest observed runtime for the app instead of letting the most recent +older-client event hide it. Mobile rows prefer the `apollos` Segment dataset, TV rows prefer `apollos_tv`, and Roku rows prefer `apollos_roku` so the same app event is not counted twice when Segment exports overlap. TV rows show `TBD` until source metadata appears in Segment exports. Once those fields are present, diff --git a/app_versions.py b/app_versions.py index b895f84..b457b41 100644 --- a/app_versions.py +++ b/app_versions.py @@ -526,7 +526,11 @@ def _annotate_version_status( source_version = _string_value(row.get("source_version")) source_revision = _string_value(row.get("source_revision")) release_version = _string_value(row.get("canonical_source_version")) - latest_version = latest_by_platform.get(platform) + latest_version = ( + latest_by_platform.get(platform) + if platform in {"ios", "android", *RELEASE_TAG_PLATFORMS} + else None + ) is_outdated = False freshness_display = version or "TBD" revision_status = None @@ -538,23 +542,39 @@ def _annotate_version_status( freshness_display = source_revision[:7] if source_revision else "TBD" revision_status = roku_statuses.get(source_revision or "") is_outdated = revision_status == "behind" - elif version and latest_version: + elif platform in {"ios", "android"} and version and latest_version: is_outdated = compare_versions(version, latest_version) < 0 updated["is_outdated"] = is_outdated updated["freshness_display"] = freshness_display - if freshness_display == "TBD": - version_status_label = "TBD" - version_status_class = "observed" - elif platform == "roku" and revision_status not in {"ahead", "behind", "identical"}: - version_status_label = "Unverified" if revision_status else "Observed" - version_status_class = "observed" - elif platform in RELEASE_TAG_PLATFORMS and not release_version: + updated["comparison_display"] = ( + (_string_value(source_context.get("roku_target_revision")) or "")[:7] or "unknown" + if platform == "roku" + else latest_version or "unknown" + ) + if platform == "roku": + version_status_label = { + "behind": "Behind source", + "identical": "At source", + "ahead": "Ahead of source", + }.get(revision_status or "", "Unverified") + elif ( + platform not in {"ios", "android", *RELEASE_TAG_PLATFORMS} + or (platform in RELEASE_TAG_PLATFORMS and not release_version) + or freshness_display == "TBD" + ): version_status_label = "Unverified" - version_status_class = "observed" + elif latest_version: + version_status_label = "Behind top seen" if is_outdated else "Top seen" else: - version_status_label = "Outdated" if is_outdated else "Current" - version_status_class = "outdated" if is_outdated else "current" + version_status_label = "Unverified" + version_status_class = ( + "observed" + if version_status_label == "Unverified" + else "outdated" + if is_outdated + else "current" + ) updated["version_status_label"] = version_status_label updated["version_status_class"] = version_status_class updated["latest_seen_display"] = format_timestamp(row.get("latest_seen_at")) @@ -588,6 +608,7 @@ def _enrich_app_store_versions(rows: list[dict[str, Any]]) -> list[dict[str, Any except ValueError as exc: logging.warning("App Store version lookup returned invalid JSON: %s", exc) + checked_at = format_timestamp(datetime.now().astimezone()) enriched = [] for row in rows: updated = dict(row) @@ -600,6 +621,7 @@ def _enrich_app_store_versions(rows: list[dict[str, Any]]) -> list[dict[str, Any if store_version and (version := _string_value(store_version.get("version"))): updated["latest_app_version"] = version updated["latest_app_version_source"] = "app_store" + updated["store_checked_display"] = checked_at updated["latest_app_version_seen_at"] = store_version.get("currentVersionReleaseDate") updated["latest_app_name"] = store_version.get("trackName") else: diff --git a/templates/app_versions.html b/templates/app_versions.html index 71bf487..06029af 100644 --- a/templates/app_versions.html +++ b/templates/app_versions.html @@ -35,8 +35,7 @@ vertical-align: top; } - .version-table td:first-child, - .version-table td:nth-child(2) { + .version-table td:not(:last-child) { white-space: normal; } @@ -100,10 +99,6 @@ font-size: 0.9rem; } - .version-app-detail { - white-space: nowrap; - } - .version-empty p:last-child, .version-setup p:last-child { margin-bottom: 0; @@ -116,8 +111,10 @@

Apps

- Production freshness observed through Segment over the last {{ lookback_days }} days: - Expo runtime for mobile, release tag for TV, and source commit for Roku. + One build per app observed in Segment over the last {{ lookback_days }} days (highest runtime, + not necessarily the newest install). Freshness compares against the top version seen on the + same platform, or the source commit for Roku—not against a store release. Only iOS has a + public Apple lookup (US), which may lag App Store Connect; other stores are not checked.

@@ -143,7 +140,7 @@

App data is unavailable

{{ tab.label }} {{ tab.row_count }} {% if tab.outdated_count %} - {{ tab.outdated_count }} + {{ tab.outdated_count }} {% endif %} {% endfor %} @@ -163,37 +160,42 @@

App data is unavailable

Church App + Seen build + {% if tab.key == "ios" %}Apple lookup (US){% endif %} {{ "Expo Runtime" if tab.key in ("ios", "android") else "Release" if tab.key in ("amazon", "androidtv", "tv", "tvos") else "Commit" if tab.key == "roku" else "Version" }} - Status - Seen - Users - Events + Source freshness {% for row in tab.rows %} - {{ "Demo" if row.church == "apollos_demo" else row.church }}
- {{ row.bundle_id }} + {{ row.church }} + + {{ row.application_name }}
{{ row.bundle_id }} + + {{ row.app_version or "unknown" }}
+ Last seen {{ row.latest_seen_display or "unknown" }} + {% if tab.key == "ios" %} + + {% if row.latest_app_version_source == "app_store" %} + {{ row.latest_app_version }}
+ Checked {{ row.store_checked_display or "unknown" }} + {% else %} + Not available + {% endif %} + + {% endif %} - {{ row.application_name }}
- Observed {{ row.app_version or "unknown" }} - {% if row.latest_app_version_source == "app_store" and row.latest_app_version != row.app_version %} -
- App Store (live) {{ row.latest_app_version }} - {% endif %} + {{ row.freshness_display or "TBD" }}
+ {{ "Source target" if tab.key == "roku" else "Top seen" }} {{ row.comparison_display or "unknown" }} - {{ row.freshness_display or row.apollos_version or "TBD" }} - - {{ row.version_status_label or ("Outdated" if row.is_outdated else "Current") }} + + {{ row.version_status_label or "Unverified" }} - {{ row.latest_seen_display or "unknown" }} - {{ row.user_count }} - {{ row.event_count }} {% endfor %} diff --git a/tests/test_app_versions.py b/tests/test_app_versions.py index 652666e..648ec77 100644 --- a/tests/test_app_versions.py +++ b/tests/test_app_versions.py @@ -191,7 +191,10 @@ def test_annotates_outdated_apps_by_platform_latest_runtime(self): ) annotated = app_versions._annotate_version_status( selected, - {"roku_revision_statuses": {rows[-1]["source_revision"]: "behind"}}, + { + "roku_revision_statuses": {rows[-1]["source_revision"]: "behind"}, + "roku_target_revision": "deadbeefabcdef0", + }, ) one_church = next(row for row in annotated if row["church"] == "one-church") @@ -203,19 +206,25 @@ def test_annotates_outdated_apps_by_platform_latest_runtime(self): roku_church = next(row for row in annotated if row["church"] == "roku-church") self.assertTrue(one_church["is_outdated"]) self.assertEqual(one_church["freshness_display"], "97") + self.assertEqual(one_church["comparison_display"], "101") + self.assertEqual(one_church["version_status_label"], "Behind top seen") self.assertFalse(two_church["is_outdated"]) - self.assertEqual(tv_church["version_status_label"], "TBD") + self.assertEqual(two_church["version_status_label"], "Top seen") + self.assertEqual(tv_church["version_status_label"], "Unverified") self.assertTrue(old_tv_church["is_outdated"]) self.assertEqual(old_tv_church["freshness_display"], "v2026.05.01.00") self.assertFalse(new_tv_church["is_outdated"]) self.assertFalse(unknown_platform["is_outdated"]) + self.assertEqual(unknown_platform["version_status_label"], "Unverified") self.assertTrue(roku_church["is_outdated"]) + self.assertEqual(roku_church["version_status_label"], "Behind source") + self.assertEqual(roku_church["comparison_display"], "deadbee") self.assertEqual(roku_church["freshness_display"], "ba95e2f") self.assertEqual(annotated[0]["church"], "one-church") self.assertTrue(app_versions._revisions_match("abcdef123456", "abcdef1")) self.assertFalse(app_versions._revisions_match("abcdef123456", "abc")) - def test_annotates_outdated_apps_by_app_store_version(self): + def test_store_lookup_does_not_claim_a_build_is_outdated(self): rows = [ { "church": "bayside", @@ -248,6 +257,7 @@ def test_annotates_outdated_apps_by_app_store_version(self): bayside = next(row for row in annotated if row["church"] == "bayside") red_rocks = next(row for row in annotated if row["church"] == "red-rocks") self.assertFalse(bayside["is_outdated"]) + self.assertEqual(bayside["version_status_label"], "Top seen") self.assertFalse(red_rocks["is_outdated"]) def test_enriches_app_store_versions_by_bundle_id(self): @@ -290,6 +300,7 @@ def test_enriches_app_store_versions_by_bundle_id(self): self.assertEqual(bayside["latest_app_version"], "5.20.30") self.assertEqual(bayside["latest_app_version_source"], "app_store") self.assertEqual(bayside["latest_app_version_seen_at"], "2026-04-14T16:34:35Z") + self.assertIsNotNone(bayside["store_checked_display"]) self.assertEqual(android["latest_app_version"], "1.0.0") self.assertEqual(android["latest_app_version_source"], "observed") @@ -703,13 +714,11 @@ def test_apps_route_renders_platform_tabs(self): "app_version": "1.0.0", "latest_app_version": "1.0.1", "latest_app_version_source": "app_store", - "latest_app_version_source_label": "App Store", + "store_checked_display": "2026-05-12 10:15 AM EDT", "apollos_platform": "ios", "apollos_version": "97", - "latest_apollos_version": "101", - "source_display": "v2026.05.12.00 (abc1234)", "is_outdated": True, - "latest_seen_display": "2026-05-12 10:00 AM EDT", + "latest_seen_at": "2026-05-12 10:00 AM EDT", "user_count": 5, "event_count": 10, }, @@ -720,17 +729,25 @@ def test_apps_route_renders_platform_tabs(self): "app_version": "1.0.0", "latest_app_version": "1.0.0", "latest_app_version_source": "observed", - "latest_app_version_source_label": "Observed", "apollos_platform": "android", "apollos_version": "97", - "latest_apollos_version": "97", - "source_display": "TBD", "is_outdated": False, - "latest_seen_display": "2026-05-12 10:00 AM EDT", + "latest_seen_at": "2026-05-12 10:00 AM EDT", "user_count": 7, "event_count": 12, }, ] + rows.append( + { + "church": "three-church", + "bundle_id": "com.three", + "application_name": "Three Church", + "app_version": "1.0.2", + "apollos_platform": "ios", + "apollos_version": "101", + } + ) + rows = app_versions._annotate_version_status(rows) context = { "status": "ready", "status_label": "Ready", @@ -752,21 +769,21 @@ def test_apps_route_renders_platform_tabs(self): self.assertIn('data-version-tab="ios"', body) self.assertIn('data-version-tab="android"', body) self.assertIn('id="version-panel-ios"', body) - self.assertNotIn("Latest observed", body) self.assertIn("One Church", body) - self.assertNotIn("Latest App", body) - self.assertNotIn("Observed Version", body) + self.assertIn("Seen build", body) + self.assertIn("Apple lookup (US)", body) self.assertIn("Expo Runtime", body) - self.assertNotIn("Source", body) - self.assertIn("1.0.1", body) - self.assertIn("App Store (live) 1.0.1", body) - self.assertNotIn("App Store (live) 1.0.0", body) + self.assertIn("Top seen 101", body) + self.assertIn("Source freshness", body) + self.assertIn("Checked 2026-05-12 10:15 AM EDT", body) + self.assertIn("Last seen 2026-05-12 10:00 AM EDT", body) + self.assertIn("Behind top seen", body) + self.assertIn("Not available", body) self.assertIn("97", body) self.assertIn("Two Church", body) - self.assertNotIn("Platform", body) - self.assertNotIn("Latest Observed", body) + self.assertNotIn("App Store (live)", body) - def test_preview_shows_demo_label_and_public_app_store_version(self): + def test_preview_shows_church_slug_and_distinguishes_seen_from_apple_lookup(self): row = { "church": "apollos_demo", "bundle_id": "com.differential.apollospreview", @@ -774,21 +791,26 @@ def test_preview_shows_demo_label_and_public_app_store_version(self): "app_version": "1.0.0", "latest_app_version": "1.40", "latest_app_version_source": "app_store", + "store_checked_display": "2026-09-25 08:45 PM EDT", "apollos_platform": "ios", - "freshness_display": "106", + "apollos_version": "106", } + rows = app_versions._annotate_version_status([row]) context = { "status": "ready", - "rows": [row], - "platform_tabs": app_versions.build_platform_tabs([row]), + "rows": rows, + "platform_tabs": app_versions.build_platform_tabs(rows), "lookback_days": 30, } with patch.object(app_module, "get_app_versions_context", return_value=context): body = self.client.get("/apps").get_data(as_text=True) - self.assertIn("Demo", body) - self.assertIn("Observed 1.0.0", body) - self.assertIn("App Store (live) 1.40", body) - self.assertNotIn("apollos_demo", body) + self.assertIn("apollos_demo", body) + self.assertIn("Apollos Preview", body) + self.assertIn("Last seen unknown", body) + self.assertIn("Checked 2026-09-25 08:45 PM EDT", body) + self.assertIn("Top seen", body) + self.assertIn("1.40", body) + self.assertNotIn("App Store (live)", body) if __name__ == "__main__": From 16369a59049988c3eb3500cf24ff3afbceae7789 Mon Sep 17 00:00:00 2001 From: Michael Neeley Date: Fri, 25 Sep 2026 21:00:58 -0400 Subject: [PATCH 2/8] Treat malformed mobile runtimes as unverified --- README.md | 6 +++--- app_versions.py | 11 ++++++++++- templates/app_versions.html | 7 ++++--- tests/test_app_versions.py | 12 ++++++++++++ 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0680376..5963624 100644 --- a/README.md +++ b/README.md @@ -220,9 +220,9 @@ The page first inspects `INFORMATION_SCHEMA.COLUMNS` for the configured Segment queries tables that expose a supported version signal, so Segment lifecycle-only app-store `version` fields are not mistaken for Apollos runtime versions. Source freshness means *behind top seen* or *top seen* within the same platform, not behind an App Store release. Missing or -uncomparable signals are *unverified*. If older clients are still active after a release, the -dashboard keeps the highest observed runtime for the app instead of letting the most recent -older-client event hide it. Mobile rows prefer the +uncomparable signals (including malformed mobile runtimes) are *unverified*. Mobile rows keep +the highest comparable runtime instead of letting a recent older-client event hide it; TV selects +the highest stable release tag and Roku selects its source version. Mobile rows prefer the `apollos` Segment dataset, TV rows prefer `apollos_tv`, and Roku rows prefer `apollos_roku` so the same app event is not counted twice when Segment exports overlap. TV rows show `TBD` until source metadata appears in Segment exports. Once those fields are present, diff --git a/app_versions.py b/app_versions.py index b457b41..2d74e7b 100644 --- a/app_versions.py +++ b/app_versions.py @@ -57,6 +57,7 @@ STABLE_RELEASE_TAG_PATTERN = re.compile(r"^v\d{4}\.\d{2}\.\d{2}\.\d{2}$") ALPHA_RELEASE_TAG_PATTERN = re.compile(r"^(v\d{4}\.\d{2}\.\d{2}\.\d{2})-alpha\.\d+$") SHA_PATTERN = re.compile(r"^[0-9a-fA-F]{7,40}$") +RUNTIME_VERSION_PATTERN = re.compile(r"\d+(?:\.\d+)*") INTERNAL_DEPLOYMENT_TRACKS = {"beta", "development", "internal", "preview", "prerelease"} @@ -512,6 +513,12 @@ def _annotate_version_status( else "apollos_version" ) ) + if ( + platform in {"ios", "android"} + and version + and not RUNTIME_VERSION_PATTERN.fullmatch(version) + ): + continue if version and ( platform not in latest_by_platform or compare_versions(version, latest_by_platform[platform]) > 0 @@ -523,6 +530,7 @@ def _annotate_version_status( updated = dict(row) platform = (_string_value(row.get("apollos_platform")) or "unknown").lower() version = _string_value(row.get("apollos_version")) + comparable_runtime = bool(version and RUNTIME_VERSION_PATTERN.fullmatch(version)) source_version = _string_value(row.get("source_version")) source_revision = _string_value(row.get("source_revision")) release_version = _string_value(row.get("canonical_source_version")) @@ -542,7 +550,7 @@ def _annotate_version_status( freshness_display = source_revision[:7] if source_revision else "TBD" revision_status = roku_statuses.get(source_revision or "") is_outdated = revision_status == "behind" - elif platform in {"ios", "android"} and version and latest_version: + elif platform in {"ios", "android"} and comparable_runtime and latest_version and version: is_outdated = compare_versions(version, latest_version) < 0 updated["is_outdated"] = is_outdated @@ -561,6 +569,7 @@ def _annotate_version_status( elif ( platform not in {"ios", "android", *RELEASE_TAG_PLATFORMS} or (platform in RELEASE_TAG_PLATFORMS and not release_version) + or (platform in {"ios", "android"} and not comparable_runtime) or freshness_display == "TBD" ): version_status_label = "Unverified" diff --git a/templates/app_versions.html b/templates/app_versions.html index 06029af..05b1220 100644 --- a/templates/app_versions.html +++ b/templates/app_versions.html @@ -111,9 +111,10 @@

Apps

- One build per app observed in Segment over the last {{ lookback_days }} days (highest runtime, - not necessarily the newest install). Freshness compares against the top version seen on the - same platform, or the source commit for Roku—not against a store release. Only iOS has a + One build per app observed in Segment over the last {{ lookback_days }} days: mobile selects + the highest runtime, TV the highest stable release tag, and Roku its source version. This may + not be the newest install. Freshness compares with the top version seen on the same platform, + or the source commit for Roku—not a store release. Only iOS has a public Apple lookup (US), which may lag App Store Connect; other stores are not checked.

diff --git a/tests/test_app_versions.py b/tests/test_app_versions.py index 648ec77..8744197 100644 --- a/tests/test_app_versions.py +++ b/tests/test_app_versions.py @@ -132,6 +132,14 @@ def test_annotates_outdated_apps_by_platform_latest_runtime(self): "version_source": "runtime", "latest_seen_at": datetime(2026, 5, 2, 12, 0, tzinfo=timezone.utc), }, + { + "church": "bad-runtime", + "apollos_platform": "ios", + "application_name": "Bad Runtime", + "bundle_id": "com.bad", + "apollos_version": "v999", + "latest_seen_at": datetime(2026, 5, 3, 12, 0, tzinfo=timezone.utc), + }, { "church": "tv-church", "apollos_platform": "tvos", @@ -199,6 +207,7 @@ def test_annotates_outdated_apps_by_platform_latest_runtime(self): one_church = next(row for row in annotated if row["church"] == "one-church") two_church = next(row for row in annotated if row["church"] == "two-church") + bad_runtime = next(row for row in annotated if row["church"] == "bad-runtime") tv_church = next(row for row in annotated if row["church"] == "tv-church") old_tv_church = next(row for row in annotated if row["church"] == "old-tv-church") new_tv_church = next(row for row in annotated if row["church"] == "new-tv-church") @@ -210,6 +219,9 @@ def test_annotates_outdated_apps_by_platform_latest_runtime(self): self.assertEqual(one_church["version_status_label"], "Behind top seen") self.assertFalse(two_church["is_outdated"]) self.assertEqual(two_church["version_status_label"], "Top seen") + self.assertEqual(bad_runtime["version_status_label"], "Unverified") + self.assertEqual(bad_runtime["comparison_display"], "101") + self.assertFalse(bad_runtime["is_outdated"]) self.assertEqual(tv_church["version_status_label"], "Unverified") self.assertTrue(old_tv_church["is_outdated"]) self.assertEqual(old_tv_church["freshness_display"], "v2026.05.01.00") From 619d825f5c25a5ca2cda9c2266ff1939e3e620eb Mon Sep 17 00:00:00 2001 From: Michael Neeley Date: Fri, 25 Sep 2026 21:09:18 -0400 Subject: [PATCH 3/8] Keep valid mobile runtimes and drop unused query totals --- app_versions.py | 33 +++++++++++++-------------------- tests/test_app_versions.py | 20 ++++++++++++++++++-- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/app_versions.py b/app_versions.py index 2d74e7b..e29a563 100644 --- a/app_versions.py +++ b/app_versions.py @@ -48,8 +48,6 @@ "source_revision": ("source_revision", "sourceRevision"), "source_version": ("source_version", "sourceVersion"), "deployment_track": ("deployment_track", "deploymentTrack"), - "user_id": ("user_id", "userId"), - "anonymous_id": ("anonymous_id", "anonymousId"), } ROKU_ANALYTICS_VERSION_CANDIDATES = ("context_library_version",) @@ -304,9 +302,7 @@ def _build_app_versions_query( deployment_track, source_dataset, source_table, - version_source, - user_id, - anonymous_id + version_source FROM version_events WHERE apollos_version IS NOT NULL AND apollos_version != '' ), @@ -362,8 +358,7 @@ def _build_app_versions_query( events.source_dataset, events.source_table, events.version_source, - MAX(events.seen_at) AS latest_seen_at, - COUNT(*) AS version_event_count + MAX(events.seen_at) AS latest_seen_at FROM app_identity_events events JOIN display_churches USING (app_identity_key) @@ -382,14 +377,6 @@ def _build_app_versions_query( events.source_dataset, events.source_table, events.version_source - ), - app_totals AS ( - SELECT - app_identity_key, - COUNT(*) AS event_count, - COUNT(DISTINCT COALESCE(user_id, anonymous_id)) AS user_count - FROM app_identity_events - GROUP BY app_identity_key ) SELECT observation.church, @@ -405,12 +392,8 @@ def _build_app_versions_query( observation.source_dataset, observation.source_table, observation.version_source, - observation.latest_seen_at, - totals.event_count, - totals.user_count + observation.latest_seen_at FROM version_observations observation - JOIN app_totals totals - USING (app_identity_key) ORDER BY observation.latest_seen_at DESC """ query_config = _query_job_config( @@ -746,6 +729,16 @@ def _is_newer_observed_version( ) -> bool: candidate_version = _string_value(candidate.get("apollos_version")) current_version = _string_value(current.get("apollos_version")) + platform = (_string_value(candidate.get("apollos_platform")) or "unknown").lower() + if platform in {"ios", "android"}: + candidate_valid = bool( + candidate_version and RUNTIME_VERSION_PATTERN.fullmatch(candidate_version) + ) + current_valid = bool(current_version and RUNTIME_VERSION_PATTERN.fullmatch(current_version)) + if candidate_valid != current_valid: + return candidate_valid + if not candidate_valid: + candidate_version = current_version = None if candidate_version and current_version: version_compare = compare_versions(candidate_version, current_version) if version_compare != 0: diff --git a/tests/test_app_versions.py b/tests/test_app_versions.py index 8744197..b41b47c 100644 --- a/tests/test_app_versions.py +++ b/tests/test_app_versions.py @@ -487,13 +487,27 @@ def test_selects_highest_observed_version_instead_of_most_recent_event(self): }, ] rows.append({**rows[0], "apollos_version": "999", "deployment_track": "internal"}) + rows.append({**rows[0], "apollos_version": "v999", "app_version": "1.0.99"}) + rows.append( + { + **rows[2], + "church": "only_bad", + "bundle_id": "com.apollos.bad", + "apollos_version": "v999", + } + ) selected = app_versions._select_latest_observed_versions(rows) grow_church = next(row for row in selected if row["church"] == "grow_church") - self.assertEqual(len(selected), 2) + only_bad = next(row for row in selected if row["church"] == "only_bad") + self.assertEqual(len(selected), 3) self.assertEqual(grow_church["apollos_version"], "97") self.assertEqual(grow_church["app_version"], "1.0.31") + self.assertEqual( + app_versions._annotate_version_status([only_bad])[0]["version_status_label"], + "Unverified", + ) def test_app_identity_uses_bundle_when_application_name_changes(self): rows = [ @@ -660,7 +674,9 @@ def test_builds_query_from_discovered_segment_columns(self): self.assertIn("app_identity_events AS", query) self.assertIn("display_churches AS", query) self.assertIn("version_observations AS", query) - self.assertIn("app_totals AS", query) + self.assertNotIn("app_totals AS", query) + self.assertNotIn("COUNT(DISTINCT", query) + self.assertNotIn("AS user_count", query) self.assertIn("MAX(events.seen_at) AS latest_seen_at", query) self.assertIn("GROUP BY app_identity_key", query) self.assertIn("USING (app_identity_key)", query) From 0828ef5eb71586cccc048df82646ddafc7fcc282 Mon Sep 17 00:00:00 2001 From: Michael Neeley Date: Fri, 25 Sep 2026 21:15:45 -0400 Subject: [PATCH 4/8] Explain Roku labels and hide unverified comparisons --- README.md | 6 ++++-- templates/app_versions.html | 8 +++++++- tests/test_app_versions.py | 22 +++++++++++++++++++++- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5963624..8512b08 100644 --- a/README.md +++ b/README.md @@ -219,8 +219,10 @@ build or a measure of all installations. Roku Segment exports currently do not e The page first inspects `INFORMATION_SCHEMA.COLUMNS` for the configured Segment tables and only queries tables that expose a supported version signal, so Segment lifecycle-only app-store `version` fields are not mistaken for Apollos runtime versions. Source freshness means *behind -top seen* or *top seen* within the same platform, not behind an App Store release. Missing or -uncomparable signals (including malformed mobile runtimes) are *unverified*. Mobile rows keep +top seen* or *top seen* within the same platform; Roku uses *behind source*, *at source*, or +*ahead of source* against the target commit. None of these labels compare against a store release. +Missing or uncomparable signals (including malformed mobile runtimes) are *unverified* and show +no comparison. Mobile rows keep the highest comparable runtime instead of letting a recent older-client event hide it; TV selects the highest stable release tag and Roku selects its source version. Mobile rows prefer the `apollos` Segment dataset, TV rows prefer `apollos_tv`, and Roku rows prefer `apollos_roku` so the diff --git a/templates/app_versions.html b/templates/app_versions.html index 05b1220..7be988b 100644 --- a/templates/app_versions.html +++ b/templates/app_versions.html @@ -190,7 +190,13 @@

App data is unavailable

{% endif %} {{ row.freshness_display or "TBD" }}
- {{ "Source target" if tab.key == "roku" else "Top seen" }} {{ row.comparison_display or "unknown" }} + + {% if row.version_status_label == "Unverified" %} + Comparison unavailable + {% else %} + {{ "Source target" if tab.key == "roku" else "Top seen" }} {{ row.comparison_display or "unknown" }} + {% endif %} + diff --git a/tests/test_app_versions.py b/tests/test_app_versions.py index b41b47c..0fcf91d 100644 --- a/tests/test_app_versions.py +++ b/tests/test_app_versions.py @@ -232,6 +232,12 @@ def test_annotates_outdated_apps_by_platform_latest_runtime(self): self.assertEqual(roku_church["version_status_label"], "Behind source") self.assertEqual(roku_church["comparison_display"], "deadbee") self.assertEqual(roku_church["freshness_display"], "ba95e2f") + for status, label in (("identical", "At source"), ("ahead", "Ahead of source")): + with self.subTest(status=status): + checked = app_versions._annotate_version_status( + [rows[-1]], {"roku_revision_statuses": {rows[-1]["source_revision"]: status}} + ) + self.assertEqual(checked[0]["version_status_label"], label) self.assertEqual(annotated[0]["church"], "one-church") self.assertTrue(app_versions._revisions_match("abcdef123456", "abcdef1")) self.assertFalse(app_versions._revisions_match("abcdef123456", "abc")) @@ -823,7 +829,17 @@ def test_preview_shows_church_slug_and_distinguishes_seen_from_apple_lookup(self "apollos_platform": "ios", "apollos_version": "106", } - rows = app_versions._annotate_version_status([row]) + rows = app_versions._annotate_version_status( + [ + row, + { + **row, + "bundle_id": "com.example.invalid", + "application_name": "Bad App", + "apollos_version": "v999", + }, + ] + ) context = { "status": "ready", "rows": rows, @@ -837,6 +853,10 @@ def test_preview_shows_church_slug_and_distinguishes_seen_from_apple_lookup(self self.assertIn("Last seen unknown", body) self.assertIn("Checked 2026-09-25 08:45 PM EDT", body) self.assertIn("Top seen", body) + bad_start = body.index("com.example.invalid") + bad_row = body[bad_start : body.index("", bad_start)] + self.assertIn("Comparison unavailable", bad_row) + self.assertNotIn("Top seen 106", bad_row) self.assertIn("1.40", body) self.assertNotIn("App Store (live)", body) From 2a8de4283e41a1062d62e27d095e05bfc3beefe1 Mon Sep 17 00:00:00 2001 From: Michael Neeley Date: Fri, 25 Sep 2026 21:16:19 -0400 Subject: [PATCH 5/8] Keep status tests focused on changed behavior --- tests/test_app_versions.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_app_versions.py b/tests/test_app_versions.py index 0fcf91d..f51265d 100644 --- a/tests/test_app_versions.py +++ b/tests/test_app_versions.py @@ -232,12 +232,6 @@ def test_annotates_outdated_apps_by_platform_latest_runtime(self): self.assertEqual(roku_church["version_status_label"], "Behind source") self.assertEqual(roku_church["comparison_display"], "deadbee") self.assertEqual(roku_church["freshness_display"], "ba95e2f") - for status, label in (("identical", "At source"), ("ahead", "Ahead of source")): - with self.subTest(status=status): - checked = app_versions._annotate_version_status( - [rows[-1]], {"roku_revision_statuses": {rows[-1]["source_revision"]: status}} - ) - self.assertEqual(checked[0]["version_status_label"], label) self.assertEqual(annotated[0]["church"], "one-church") self.assertTrue(app_versions._revisions_match("abcdef123456", "abcdef1")) self.assertFalse(app_versions._revisions_match("abcdef123456", "abc")) From 515fb11b4ebbcb9d1ad5901ce1e41bd30b949ce1 Mon Sep 17 00:00:00 2001 From: Michael Neeley Date: Fri, 25 Sep 2026 21:22:43 -0400 Subject: [PATCH 6/8] Label the freshness status column --- templates/app_versions.html | 2 +- tests/test_app_versions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/app_versions.html b/templates/app_versions.html index 7be988b..33efef1 100644 --- a/templates/app_versions.html +++ b/templates/app_versions.html @@ -164,7 +164,7 @@

App data is unavailable

Seen build {% if tab.key == "ios" %}Apple lookup (US){% endif %} {{ "Expo Runtime" if tab.key in ("ios", "android") else "Release" if tab.key in ("amazon", "androidtv", "tv", "tvos") else "Commit" if tab.key == "roku" else "Version" }} - Source freshness + Status diff --git a/tests/test_app_versions.py b/tests/test_app_versions.py index f51265d..17f338d 100644 --- a/tests/test_app_versions.py +++ b/tests/test_app_versions.py @@ -802,7 +802,7 @@ def test_apps_route_renders_platform_tabs(self): self.assertIn("Apple lookup (US)", body) self.assertIn("Expo Runtime", body) self.assertIn("Top seen 101", body) - self.assertIn("Source freshness", body) + self.assertIn("Status", body) self.assertIn("Checked 2026-05-12 10:15 AM EDT", body) self.assertIn("Last seen 2026-05-12 10:00 AM EDT", body) self.assertIn("Behind top seen", body) From dffc2f4a625f6538ce6637179c76f543f770692e Mon Sep 17 00:00:00 2001 From: Michael Neeley Date: Fri, 25 Sep 2026 21:30:58 -0400 Subject: [PATCH 7/8] Timestamp attempted iOS lookups even without a result --- app_versions.py | 10 ++++------ templates/app_versions.html | 6 ++++-- tests/test_app_versions.py | 16 ++++++---------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/app_versions.py b/app_versions.py index e29a563..a1dd358 100644 --- a/app_versions.py +++ b/app_versions.py @@ -605,15 +605,13 @@ def _enrich_app_store_versions(rows: list[dict[str, Any]]) -> list[dict[str, Any for row in rows: updated = dict(row) bundle_id = _string_value(row.get("bundle_id")) - store_version = ( - app_store_versions.get(bundle_id or "") - if _should_lookup_app_store_version(row) - else None - ) + should_lookup = _should_lookup_app_store_version(row) + if should_lookup: + updated["store_checked_display"] = checked_at + store_version = app_store_versions.get(bundle_id or "") if should_lookup else None if store_version and (version := _string_value(store_version.get("version"))): updated["latest_app_version"] = version updated["latest_app_version_source"] = "app_store" - updated["store_checked_display"] = checked_at updated["latest_app_version_seen_at"] = store_version.get("currentVersionReleaseDate") updated["latest_app_name"] = store_version.get("trackName") else: diff --git a/templates/app_versions.html b/templates/app_versions.html index 33efef1..88cfc01 100644 --- a/templates/app_versions.html +++ b/templates/app_versions.html @@ -181,11 +181,13 @@

App data is unavailable

{% if tab.key == "ios" %} {% if row.latest_app_version_source == "app_store" %} - {{ row.latest_app_version }}
- Checked {{ row.store_checked_display or "unknown" }} + {{ row.latest_app_version }} {% else %} Not available {% endif %} + {% if row.store_checked_display %} +
Checked {{ row.store_checked_display }} + {% endif %} {% endif %} diff --git a/tests/test_app_versions.py b/tests/test_app_versions.py index 17f338d..1305f85 100644 --- a/tests/test_app_versions.py +++ b/tests/test_app_versions.py @@ -215,13 +215,9 @@ def test_annotates_outdated_apps_by_platform_latest_runtime(self): roku_church = next(row for row in annotated if row["church"] == "roku-church") self.assertTrue(one_church["is_outdated"]) self.assertEqual(one_church["freshness_display"], "97") - self.assertEqual(one_church["comparison_display"], "101") self.assertEqual(one_church["version_status_label"], "Behind top seen") self.assertFalse(two_church["is_outdated"]) - self.assertEqual(two_church["version_status_label"], "Top seen") self.assertEqual(bad_runtime["version_status_label"], "Unverified") - self.assertEqual(bad_runtime["comparison_display"], "101") - self.assertFalse(bad_runtime["is_outdated"]) self.assertEqual(tv_church["version_status_label"], "Unverified") self.assertTrue(old_tv_church["is_outdated"]) self.assertEqual(old_tv_church["freshness_display"], "v2026.05.01.00") @@ -230,7 +226,6 @@ def test_annotates_outdated_apps_by_platform_latest_runtime(self): self.assertEqual(unknown_platform["version_status_label"], "Unverified") self.assertTrue(roku_church["is_outdated"]) self.assertEqual(roku_church["version_status_label"], "Behind source") - self.assertEqual(roku_church["comparison_display"], "deadbee") self.assertEqual(roku_church["freshness_display"], "ba95e2f") self.assertEqual(annotated[0]["church"], "one-church") self.assertTrue(app_versions._revisions_match("abcdef123456", "abcdef1")) @@ -269,7 +264,6 @@ def test_store_lookup_does_not_claim_a_build_is_outdated(self): bayside = next(row for row in annotated if row["church"] == "bayside") red_rocks = next(row for row in annotated if row["church"] == "red-rocks") self.assertFalse(bayside["is_outdated"]) - self.assertEqual(bayside["version_status_label"], "Top seen") self.assertFalse(red_rocks["is_outdated"]) def test_enriches_app_store_versions_by_bundle_id(self): @@ -334,8 +328,9 @@ def test_looks_up_all_app_store_bundles(self): "_fetch_app_store_versions", return_value={}, ) as fetch_app_store_versions: - app_versions._enrich_app_store_versions(rows) + enriched = app_versions._enrich_app_store_versions(rows) + self.assertTrue(all(row.get("store_checked_display") for row in enriched)) lookup_bundle_ids = fetch_app_store_versions.call_args.args[0] self.assertEqual(len(lookup_bundle_ids), app_versions.APP_STORE_LOOKUP_LIMIT + 1) self.assertIn(f"com.example.{app_versions.APP_STORE_LOOKUP_LIMIT}", lookup_bundle_ids) @@ -675,8 +670,6 @@ def test_builds_query_from_discovered_segment_columns(self): self.assertIn("display_churches AS", query) self.assertIn("version_observations AS", query) self.assertNotIn("app_totals AS", query) - self.assertNotIn("COUNT(DISTINCT", query) - self.assertNotIn("AS user_count", query) self.assertIn("MAX(events.seen_at) AS latest_seen_at", query) self.assertIn("GROUP BY app_identity_key", query) self.assertIn("USING (app_identity_key)", query) @@ -771,6 +764,7 @@ def test_apps_route_renders_platform_tabs(self): "bundle_id": "com.three", "application_name": "Three Church", "app_version": "1.0.2", + "store_checked_display": "2026-05-12 10:16 AM EDT", "apollos_platform": "ios", "apollos_version": "101", } @@ -806,7 +800,9 @@ def test_apps_route_renders_platform_tabs(self): self.assertIn("Checked 2026-05-12 10:15 AM EDT", body) self.assertIn("Last seen 2026-05-12 10:00 AM EDT", body) self.assertIn("Behind top seen", body) - self.assertIn("Not available", body) + unavailable = body[body.index("com.three") : body.index("", body.index("com.three"))] + self.assertIn("Not available", unavailable) + self.assertIn("Checked 2026-05-12 10:16 AM EDT", unavailable) self.assertIn("97", body) self.assertIn("Two Church", body) self.assertNotIn("App Store (live)", body) From 1d082f25a907bd1ede550c6f7f3cba0f50658535 Mon Sep 17 00:00:00 2001 From: Michael Neeley Date: Fri, 25 Sep 2026 21:37:04 -0400 Subject: [PATCH 8/8] Cover remaining Roku source statuses --- tests/test_app_versions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_app_versions.py b/tests/test_app_versions.py index 1305f85..88a34ae 100644 --- a/tests/test_app_versions.py +++ b/tests/test_app_versions.py @@ -226,6 +226,11 @@ def test_annotates_outdated_apps_by_platform_latest_runtime(self): self.assertEqual(unknown_platform["version_status_label"], "Unverified") self.assertTrue(roku_church["is_outdated"]) self.assertEqual(roku_church["version_status_label"], "Behind source") + for status, label in (("identical", "At source"), ("ahead", "Ahead of source")): + checked = app_versions._annotate_version_status( + [rows[-1]], {"roku_revision_statuses": {rows[-1]["source_revision"]: status}} + ) + self.assertEqual(checked[0]["version_status_label"], label) self.assertEqual(roku_church["freshness_display"], "ba95e2f") self.assertEqual(annotated[0]["church"], "one-church") self.assertTrue(app_versions._revisions_match("abcdef123456", "abcdef1")) @@ -795,17 +800,12 @@ def test_apps_route_renders_platform_tabs(self): self.assertIn("Seen build", body) self.assertIn("Apple lookup (US)", body) self.assertIn("Expo Runtime", body) - self.assertIn("Top seen 101", body) self.assertIn("Status", body) - self.assertIn("Checked 2026-05-12 10:15 AM EDT", body) - self.assertIn("Last seen 2026-05-12 10:00 AM EDT", body) - self.assertIn("Behind top seen", body) unavailable = body[body.index("com.three") : body.index("", body.index("com.three"))] self.assertIn("Not available", unavailable) self.assertIn("Checked 2026-05-12 10:16 AM EDT", unavailable) self.assertIn("97", body) self.assertIn("Two Church", body) - self.assertNotIn("App Store (live)", body) def test_preview_shows_church_slug_and_distinguishes_seen_from_apple_lookup(self): row = {