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
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""add version to daily_activity

Revision ID: 9a5e8c7d4b21
Revises: 0563d6d01623
Create Date: 2026-09-10 00:00:00.000000

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
import sqlmodel


revision: str = "9a5e8c7d4b21"
down_revision: Union[str, Sequence[str], None] = "0563d6d01623"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
op.add_column(
"daily_activity",
sa.Column(
"version",
sqlmodel.sql.sqltypes.AutoString(length=32),
nullable=False,
server_default="",
),
)
op.create_index(
op.f("ix_daily_activity_version"),
"daily_activity",
["version"],
unique=False,
)


def downgrade() -> None:
"""Downgrade schema."""
op.drop_index(op.f("ix_daily_activity_version"), table_name="daily_activity")
op.drop_column("daily_activity", "version")
1 change: 1 addition & 0 deletions backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,4 @@ class DailyActivity(SQLModel, table=True):

installation_id: str = Field(max_length=64, index=True)
activity_date: str = Field(max_length=10, index=True)
version: str = Field(default="", max_length=32, index=True)
20 changes: 10 additions & 10 deletions backend/app/routers/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,18 +143,18 @@ def _bucket(floor: datetime, *, upper: datetime | None = None) -> int:


def _version_mix(session: Session, now) -> list[VersionMixEntry]:
"""Version distribution of active installations per day, last 30 days."""
cutoff = now - timedelta(days=30)
"""Version distribution observed per day, last 30 days."""
cutoff = (now - timedelta(days=30)).date().isoformat()
rows = session.exec(
select(
func.date(col(Installation.last_seen_at)),
col(Installation.version),
func.count(col(Installation.installation_id)),
col(DailyActivity.activity_date),
col(DailyActivity.version),
func.count(col(DailyActivity.installation_id)),
)
.where(col(Installation.last_seen_at) >= cutoff)
.where(col(Installation.version) != "")
.group_by(func.date(col(Installation.last_seen_at)), col(Installation.version))
.order_by(func.date(col(Installation.last_seen_at)))
.where(col(DailyActivity.activity_date) >= cutoff)
.where(col(DailyActivity.version) != "")
.group_by(col(DailyActivity.activity_date), col(DailyActivity.version))
.order_by(col(DailyActivity.activity_date))
).all()

by_date: dict[str, list[StatEntry]] = {}
Expand Down Expand Up @@ -191,4 +191,4 @@ async def get_stats(
longevity=_longevity(session, now),
version_mix=_version_mix(session, now),
server_version=__version__,
)
)
33 changes: 19 additions & 14 deletions backend/app/routers/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,20 +76,6 @@ async def ingest_telemetry(
now = utcnow()
existing = session.get(Installation, payload.installation_id)

# Record this installation as active today (one row per install per day).
today = now.date().isoformat()
activity = session.exec(
select(DailyActivity).where(
col(DailyActivity.installation_id) == payload.installation_id,
col(DailyActivity.activity_date) == today,
)
).first()
if activity is None:
session.add(DailyActivity(
installation_id=payload.installation_id,
activity_date=today,
))

if existing is None:
# If this installation was pruned for inactivity, resurrect it: move it
# back to the live table so it is not double-counted in the all-time
Expand All @@ -116,5 +102,24 @@ async def ingest_telemetry(
_apply_version_specific(existing, payload)
existing.last_seen_at = now

# Record the effective version for this installation on each ping day.
# Repeated pings on the same day update the snapshot if the installation
# upgrades during that day.
today = now.date().isoformat()
activity = session.exec(
select(DailyActivity).where(
col(DailyActivity.installation_id) == payload.installation_id,
col(DailyActivity.activity_date) == today,
)
).first()
if activity is None:
session.add(DailyActivity(
installation_id=payload.installation_id,
activity_date=today,
version=existing.version,
))
else:
activity.version = existing.version

session.commit()
return TelemetryOut(installation_id=payload.installation_id)
6 changes: 4 additions & 2 deletions backend/scripts/seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ def main(count: int) -> None:
now,
)
install_id = f"seed-{i:04d}"
version = random.choice(_VERSIONS)
session.add(
Installation(
installation_id=install_id,
version=random.choice(_VERSIONS),
version=version,
os=random.choice(_OS),
architecture=random.choice(_ARCHS),
runtime=random.choice(_RUNTIMES),
Expand All @@ -66,6 +67,7 @@ def main(count: int) -> None:
DailyActivity(
installation_id=install_id,
activity_date=activity_date,
version=version,
)
)
added += 1
Expand All @@ -75,4 +77,4 @@ def main(count: int) -> None:

if __name__ == "__main__":
count = int(sys.argv[1]) if len(sys.argv) > 1 else 25
main(count)
main(count)
43 changes: 41 additions & 2 deletions backend/tests/test_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ def _seed(session: Session, installation_id: str, **overrides) -> None:
session.add(Installation(**data))
# Mirror the ingest behaviour: one DailyActivity row per day the install pings.
activity_date = data["last_seen_at"].date().isoformat()
session.add(DailyActivity(installation_id=installation_id, activity_date=activity_date))
session.add(DailyActivity(
installation_id=installation_id,
activity_date=activity_date,
version=data["version"],
))


def test_empty_stats(client: TestClient):
Expand Down Expand Up @@ -204,4 +208,39 @@ def test_daily_activity_counts_each_ping_day(client: TestClient, session: Sessio

by_date = {e["date"]: e["count"] for e in body["daily"]}
assert by_date.get(yesterday) == 2 # pinger + yesterday-only
assert by_date.get(today) == 1 # only pinger
assert by_date.get(today) == 1 # only pinger


def test_version_mix_keeps_history_for_daily_pings(client: TestClient, session: Session):
"""A later ping must not replace an earlier day's version snapshot."""
now = utcnow()
yesterday = (now - timedelta(days=1)).date().isoformat()
session.add(Installation(
installation_id="upgraded",
version="v2.0.0",
os="Linux",
architecture="x64",
runtime="docker",
first_seen_at=now - timedelta(days=1),
last_seen_at=now,
))
session.add(DailyActivity(
installation_id="upgraded",
activity_date=yesterday,
version="v1.0.0",
))
session.add(DailyActivity(
installation_id="upgraded",
activity_date=now.date().isoformat(),
version="v2.0.0",
))
session.commit()

resp = client.get("/api/stats")
assert resp.status_code == 200
by_date = {
entry["date"]: {version["label"]: version["count"] for version in entry["versions"]}
for entry in resp.json()["version_mix"]
}
assert by_date[yesterday] == {"v1.0.0": 1}
assert by_date[now.date().isoformat()] == {"v2.0.0": 1}
16 changes: 14 additions & 2 deletions backend/tests/test_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from fastapi.testclient import TestClient
from sqlmodel import Session, select

from app.models import Installation
from app.models import DailyActivity, Installation


def _payload(**overrides):
Expand Down Expand Up @@ -33,6 +33,12 @@ def test_ingest_new_installation(client: TestClient, session: Session):
assert row.os == "Linux"
assert row.message_version == 1
assert row.first_seen_at == row.last_seen_at
activity = session.get(
DailyActivity,
("inst-001", row.last_seen_at.date().isoformat()),
)
assert activity is not None
assert activity.version == "v1.2.3"


def test_ingest_without_message_version_rejected(client: TestClient):
Expand Down Expand Up @@ -62,6 +68,12 @@ def test_ingest_updates_existing(client: TestClient, session: Session):
row = rows[0]
assert row.version == "v1.1.0"
assert (row.last_seen_at - row.first_seen_at).total_seconds() < 1
activity = session.get(
DailyActivity,
("inst-001", row.last_seen_at.date().isoformat()),
)
assert activity is not None
assert activity.version == "v1.1.0"


def test_ingest_unknown_fields_rejected(client: TestClient):
Expand Down Expand Up @@ -236,4 +248,4 @@ def test_dropped_field_reset_on_update():
_apply_version_specific(fresh, incoming)
assert fresh.message_version == 1
assert fresh.version == "v1.1.0"
assert fresh.runtime == ""
assert fresh.runtime == ""
Loading