Add navigation & usage metrics for end-user experience visibility

Instrument DB query timing (11 operations across 3 repositories),
streaming lifecycle (TTFB, duration, feature count), cache operation
latency, listing detail step breakdown, and frontend page load /
time-to-first-listing / stream download / detail load metrics.

Adds 16 new OTel instruments, extends the perf ingestion endpoint
with 4 new frontend metrics, and adds ~20 Grafana dashboard panels
across 4 new rows (DB Query Performance, Streaming Performance,
Listing Detail Breakdown, Cache Performance, Frontend Navigation).
This commit is contained in:
Viktor Barzin 2026-02-23 20:28:42 +00:00
parent 1ae00b7cbf
commit 35f1987ac1
No known key found for this signature in database
GPG key ID: 0EB088298288D958
11 changed files with 1236 additions and 26 deletions

View file

@ -1,6 +1,8 @@
from datetime import datetime, timedelta
import logging
import time
from typing import Any, Generator
from api.metrics import record_db_query
from data_access import Listing
from models.listing import (
BuyListing,
@ -55,8 +57,10 @@ class ListingRepository:
if limit:
query = query.limit(limit)
t0 = time.monotonic()
with Session(self.engine) as session:
rows = list(session.exec(query).all())
record_db_query("get_listings", model.__tablename__, time.monotonic() - t0, len(rows))
logging.debug(f"Found {len(rows)} listings")
return rows
@ -110,8 +114,11 @@ class ListingRepository:
query = sa_select(func.count(model.id))
query = self._apply_query_filters(query, model, query_parameters)
t0 = time.monotonic()
with Session(self.engine) as session:
return session.execute(query).scalar() or 0
result = session.execute(query).scalar() or 0
record_db_query("count_listings", model.__tablename__, time.monotonic() - t0, result)
return result
def stream_listings_optimized(
self,
@ -157,8 +164,10 @@ class ListingRepository:
batch_limit = min(page_size, limit - total_yielded)
query = query.order_by(model.id).limit(batch_limit)
t0 = time.monotonic()
with Session(self.engine) as session:
results = session.execute(query).fetchall()
record_db_query("stream_listings_page", model.__tablename__, time.monotonic() - t0, len(results))
if not results:
break
@ -364,6 +373,7 @@ class ListingRepository:
Checks both RentListing and BuyListing tables and returns the latest.
"""
t0 = time.monotonic()
with Session(self.engine) as session:
rent_max = session.execute(
sa_select(func.max(RentListing.last_seen))
@ -371,6 +381,7 @@ class ListingRepository:
buy_max = session.execute(
sa_select(func.max(BuyListing.last_seen))
).scalar()
record_db_query("get_last_updated", "rent", time.monotonic() - t0)
candidates = [t for t in (rent_max, buy_max) if t is not None]
return max(candidates) if candidates else None
@ -385,9 +396,12 @@ class ListingRepository:
filtering against API results.
"""
model = RentListing if listing_type == ListingType.RENT else BuyListing
t0 = time.monotonic()
with Session(self.engine) as session:
result = session.execute(sa_select(model.id))
return {row[0] for row in result.fetchall()}
ids = {row[0] for row in result.fetchall()}
record_db_query("get_listing_ids", model.__tablename__, time.monotonic() - t0, len(ids))
return ids
async def mark_seen(self, listing_id: int, listing_type: ListingType = ListingType.RENT) -> None:
query_params = QueryParameters(listing_type=listing_type)