From 9e40558d0d5cb9779a786c8ef8984d5c6cfb57a7 Mon Sep 17 00:00:00 2001 From: Michael Neeley Date: Fri, 25 Sep 2026 22:21:00 -0400 Subject: [PATCH 1/3] Separate build slug from observed church on Apps page --- README.md | 5 ++++- app_versions.py | 9 ++++++++- templates/app_versions.html | 9 +++++++-- tests/test_app_versions.py | 11 ++++++++++- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8512b08..0e96d2a 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,10 @@ The legacy `GET /airflow-fleet-health` Better Stack monitor endpoint has been re `GET /apps` reads the Segment BigQuery export and shows the highest observed Apollos 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. +`application_name`, `church`, `build_church`, `apollos_platform`, `source_revision`, and +`source_version` fields. For mobile apps, `build_church` is the configured deployment slug while +`church` is a church selected inside the app (which can differ in Preview). Older mobile events +without `build_church` show "Unknown build slug" rather than mislabeling the selected church. 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 diff --git a/app_versions.py b/app_versions.py index a1dd358..a2078ea 100644 --- a/app_versions.py +++ b/app_versions.py @@ -39,6 +39,7 @@ FIELD_CANDIDATES = { "church": ("church", "group_id", "groupId"), + "build_church": ("build_church", "buildChurch"), "apollos_platform": ("apollos_platform", "apollosPlatform", "apollosplatform"), "apollos_version": ("apollos_version", "apollosVersion"), "app_version": ("app_version", "appVersion"), @@ -278,6 +279,7 @@ def _build_app_versions_query( SELECT seen_at, COALESCE(NULLIF(church, ''), 'Unknown church') AS church, + NULLIF(build_church, '') AS build_church, COALESCE( NULLIF(apollos_platform, ''), IF(source_dataset = 'apollos_roku', 'roku', NULL), @@ -338,7 +340,9 @@ def _build_app_versions_query( church ORDER BY IF(church = 'Unknown church', 1, 0), church LIMIT 1 - )[OFFSET(0)] AS church + )[OFFSET(0)] AS church, + ARRAY_AGG(build_church IGNORE NULLS ORDER BY seen_at DESC LIMIT 1) + [SAFE_OFFSET(0)] AS build_church FROM app_identity_events GROUP BY app_identity_key ), @@ -346,6 +350,7 @@ def _build_app_versions_query( SELECT events.app_identity_key, display_churches.church, + display_churches.build_church, events.apollos_platform, events.application_name, events.bundle_id, @@ -365,6 +370,7 @@ def _build_app_versions_query( GROUP BY events.app_identity_key, display_churches.church, + display_churches.build_church, events.apollos_platform, events.application_name, events.bundle_id, @@ -380,6 +386,7 @@ def _build_app_versions_query( ) SELECT observation.church, + observation.build_church, observation.apollos_platform, observation.application_name, observation.bundle_id, diff --git a/templates/app_versions.html b/templates/app_versions.html index 88cfc01..aecb769 100644 --- a/templates/app_versions.html +++ b/templates/app_versions.html @@ -159,7 +159,7 @@

App data is unavailable

- + {% if tab.key == "ios" %}{% endif %} @@ -171,7 +171,12 @@

App data is unavailable

{% for row in tab.rows %} ", body) self.assertIn("", body) self.assertIn("", body) @@ -810,6 +816,7 @@ def test_apps_route_renders_platform_tabs(self): def test_preview_shows_church_slug_and_distinguishes_seen_from_apple_lookup(self): row = { "church": "apollos_demo", + "build_church": "apollos_preview", "bundle_id": "com.differential.apollospreview", "application_name": "Apollos Preview", "app_version": "1.0.0", @@ -838,7 +845,9 @@ def test_preview_shows_church_slug_and_distinguishes_seen_from_apple_lookup(self } with patch.object(app_module, "get_app_versions_context", return_value=context): body = self.client.get("/apps").get_data(as_text=True) - self.assertIn("apollos_demo", body) + self.assertIn("apollos_preview", body) + self.assertIn("Church seen: apollos_demo", body) + self.assertNotIn("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) From a8a81ab1e4fe6265bf3f899637e26f394505ad5d Mon Sep 17 00:00:00 2001 From: Michael Neeley Date: Sat, 26 Sep 2026 14:47:45 -0400 Subject: [PATCH 2/3] Keep build slug from versionless analytics events --- app_versions.py | 7 +++---- tests/test_app_versions.py | 4 ++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app_versions.py b/app_versions.py index a2078ea..043895e 100644 --- a/app_versions.py +++ b/app_versions.py @@ -263,8 +263,6 @@ def _build_app_versions_query( CURRENT_TIMESTAMP(), INTERVAL @lookback_days DAY ) - AND `{version_column}` IS NOT NULL - AND CAST(`{version_column}` AS STRING) != '' """ ) @@ -306,7 +304,7 @@ def _build_app_versions_query( source_table, version_source FROM version_events - WHERE apollos_version IS NOT NULL AND apollos_version != '' + WHERE apollos_version IS NOT NULL OR build_church IS NOT NULL ), filtered_events AS ( SELECT * @@ -337,7 +335,7 @@ def _build_app_versions_query( SELECT app_identity_key, ARRAY_AGG( - church + IF(apollos_version IS NOT NULL, church, NULL) IGNORE NULLS ORDER BY IF(church = 'Unknown church', 1, 0), church LIMIT 1 )[OFFSET(0)] AS church, @@ -367,6 +365,7 @@ def _build_app_versions_query( FROM app_identity_events events JOIN display_churches USING (app_identity_key) + WHERE events.apollos_version IS NOT NULL GROUP BY events.app_identity_key, display_churches.church, diff --git a/tests/test_app_versions.py b/tests/test_app_versions.py index 0bdbbbb..debb079 100644 --- a/tests/test_app_versions.py +++ b/tests/test_app_versions.py @@ -664,6 +664,10 @@ def test_builds_query_from_discovered_segment_columns(self): self.assertIn("NULLIF(CAST(`buildChurch` AS STRING), '') AS build_church", query) self.assertIn("[SAFE_OFFSET(0)] AS build_church", query) self.assertIn("observation.build_church", query) + self.assertNotIn("AND `apollos_version` IS NOT NULL", query) + self.assertIn("WHERE apollos_version IS NOT NULL OR build_church IS NOT NULL", query) + self.assertIn("IF(apollos_version IS NOT NULL, church, NULL) IGNORE NULLS", query) + self.assertIn("WHERE events.apollos_version IS NOT NULL", query) self.assertIn("'analytics_library' AS version_source", query) self.assertIn("TIMESTAMP_SUB(", query) self.assertIn("INTERVAL @lookback_days DAY", query) From e58e8db0dc0f90a5167f774347df5a6aa80774a9 Mon Sep 17 00:00:00 2001 From: Michael Neeley Date: Sat, 26 Sep 2026 14:52:53 -0400 Subject: [PATCH 3/3] Safely skip church for versionless-only groups --- app_versions.py | 2 +- tests/test_app_versions.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app_versions.py b/app_versions.py index 043895e..44b085b 100644 --- a/app_versions.py +++ b/app_versions.py @@ -338,7 +338,7 @@ def _build_app_versions_query( IF(apollos_version IS NOT NULL, church, NULL) IGNORE NULLS ORDER BY IF(church = 'Unknown church', 1, 0), church LIMIT 1 - )[OFFSET(0)] AS church, + )[SAFE_OFFSET(0)] AS church, ARRAY_AGG(build_church IGNORE NULLS ORDER BY seen_at DESC LIMIT 1) [SAFE_OFFSET(0)] AS build_church FROM app_identity_events diff --git a/tests/test_app_versions.py b/tests/test_app_versions.py index debb079..f0d5fa7 100644 --- a/tests/test_app_versions.py +++ b/tests/test_app_versions.py @@ -662,6 +662,7 @@ def test_builds_query_from_discovered_segment_columns(self): self.assertIn("CAST(NULL AS STRING) AS source_revision", query) self.assertIn("NULLIF(CAST(`groupId` AS STRING), '') AS church", query) self.assertIn("NULLIF(CAST(`buildChurch` AS STRING), '') AS build_church", query) + self.assertIn("[SAFE_OFFSET(0)] AS church", query) self.assertIn("[SAFE_OFFSET(0)] AS build_church", query) self.assertIn("observation.build_church", query) self.assertNotIn("AND `apollos_version` IS NOT NULL", query)
Church{{ "Build slug / church seen" if tab.key in ("ios", "android") else "Church seen" }} App Seen buildApple lookup (US)
- {{ row.church }} + {% if tab.key in ("ios", "android") %} + {{ row.build_church or "Unknown build slug" }}
+ Church seen: {{ row.church }} + {% else %} + {{ row.church }} + {% endif %}
{{ row.application_name }}
{{ row.bundle_id }}
diff --git a/tests/test_app_versions.py b/tests/test_app_versions.py index 88a34ae..0bdbbbb 100644 --- a/tests/test_app_versions.py +++ b/tests/test_app_versions.py @@ -612,6 +612,7 @@ def test_builds_query_from_discovered_segment_columns(self): ("apollos", "tracks"): { "timestamp": "timestamp", "church": "church", + "buildchurch": "buildChurch", "apollos_version": "apollos_version", "app_version": "app_version", "source_revision": "source_revision", @@ -660,6 +661,9 @@ def test_builds_query_from_discovered_segment_columns(self): ) self.assertIn("CAST(NULL AS STRING) AS source_revision", query) self.assertIn("NULLIF(CAST(`groupId` AS STRING), '') AS church", query) + self.assertIn("NULLIF(CAST(`buildChurch` AS STRING), '') AS build_church", query) + self.assertIn("[SAFE_OFFSET(0)] AS build_church", query) + self.assertIn("observation.build_church", query) self.assertIn("'analytics_library' AS version_source", query) self.assertIn("TIMESTAMP_SUB(", query) self.assertIn("INTERVAL @lookback_days DAY", query) @@ -797,6 +801,8 @@ def test_apps_route_renders_platform_tabs(self): self.assertIn('data-version-tab="android"', body) self.assertIn('id="version-panel-ios"', body) self.assertIn("One Church", body) + self.assertIn("Unknown build slug", body) + self.assertIn("Church seen: one-church", body) self.assertIn("Seen buildApple lookup (US)Expo Runtime