Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,18 +209,22 @@ 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; 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
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,
Expand Down
96 changes: 59 additions & 37 deletions app_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,14 @@
"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",)
RELEASE_TAG_PLATFORMS = {"amazon", "androidtv", "tv", "tvos"}
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"}


Expand Down Expand Up @@ -303,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 != ''
),
Expand Down Expand Up @@ -361,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)
Expand All @@ -381,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,
Expand All @@ -404,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(
Expand Down Expand Up @@ -512,6 +496,12 @@ def _annotate_version_status(
else "apollos_version"
)
)
if (
platform in {"ios", "android"}
and version
and not RUNTIME_VERSION_PATTERN.fullmatch(version)
):
continue
Comment thread
redreceipt marked this conversation as resolved.
if version and (
platform not in latest_by_platform
or compare_versions(version, latest_by_platform[platform]) > 0
Expand All @@ -523,10 +513,15 @@ 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"))
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
Expand All @@ -538,23 +533,40 @@ 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 comparable_runtime and latest_version and 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 (platform in {"ios", "android"} and not comparable_runtime)
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"))
Expand Down Expand Up @@ -588,15 +600,15 @@ 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)
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"
Expand Down Expand Up @@ -715,6 +727,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:
Expand Down
63 changes: 37 additions & 26 deletions templates/app_versions.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand All @@ -116,8 +111,11 @@
<div>
<h1>Apps</h1>
<p>
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: 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.
</p>
</div>
</section>
Expand All @@ -143,7 +141,7 @@ <h2>App data is unavailable</h2>
{{ tab.label }}
<span class="version-tab-count">{{ tab.row_count }}</span>
{% if tab.outdated_count %}
<span class="version-tab-alert">{{ tab.outdated_count }}</span>
<span class="version-tab-alert" aria-label="{{ tab.outdated_count }} behind">{{ tab.outdated_count }}</span>
{% endif %}
</button>
{% endfor %}
Expand All @@ -163,37 +161,50 @@ <h2>App data is unavailable</h2>
<tr>
<th>Church</th>
<th>App</th>
<th>Seen build</th>
{% if tab.key == "ios" %}<th>Apple lookup (US)</th>{% endif %}
<th>{{ "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" }}</th>
<th>Status</th>
<th>Seen</th>
<th>Users</th>
<th>Events</th>
</tr>
</thead>
<tbody>
{% for row in tab.rows %}
<tr>
<td>
<strong>{{ "Demo" if row.church == "apollos_demo" else row.church }}</strong><br />
<span class="version-muted">{{ row.bundle_id }}</span>
<strong>{{ row.church }}</strong>
</td>
<td>{{ row.application_name }}<br /><span class="version-muted">{{ row.bundle_id }}</span></td>
<td>
{{ row.application_name }}<br />
<span class="version-muted version-app-detail">Observed {{ row.app_version or "unknown" }}</span>
{% if row.latest_app_version_source == "app_store" and row.latest_app_version != row.app_version %}
<br />
<span class="version-muted version-app-detail">App Store (live) {{ row.latest_app_version }}</span>
{% endif %}
{{ row.app_version or "unknown" }}<br />
<span class="version-muted">Last seen {{ row.latest_seen_display or "unknown" }}</span>
</td>
{% if tab.key == "ios" %}
<td>
{% if row.latest_app_version_source == "app_store" %}
{{ row.latest_app_version }}
{% else %}
<span class="version-muted">Not available</span>
{% endif %}
{% if row.store_checked_display %}
<br /><span class="version-muted">Checked {{ row.store_checked_display }}</span>
{% endif %}
</td>
{% endif %}
<td>
<code>{{ row.freshness_display or "TBD" }}</code><br />
<span class="version-muted">
{% if row.version_status_label == "Unverified" %}
Comparison unavailable
{% else %}
{{ "Source target" if tab.key == "roku" else "Top seen" }} {{ row.comparison_display or "unknown" }}
{% endif %}
</span>
</td>
<td><code>{{ row.freshness_display or row.apollos_version or "TBD" }}</code></td>
<td>
<span class="version-status version-status--{{ row.version_status_class or ('outdated' if row.is_outdated else 'current') }}">
{{ row.version_status_label or ("Outdated" if row.is_outdated else "Current") }}
<span class="version-status version-status--{{ row.version_status_class or 'observed' }}">
{{ row.version_status_label or "Unverified" }}
</span>
</td>
<td>{{ row.latest_seen_display or "unknown" }}</td>
<td>{{ row.user_count }}</td>
<td>{{ row.event_count }}</td>
</tr>
{% endfor %}
</tbody>
Expand Down
Loading
Loading