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:
parent
1ae00b7cbf
commit
35f1987ac1
11 changed files with 1236 additions and 26 deletions
|
|
@ -1,5 +1,7 @@
|
|||
from datetime import datetime
|
||||
import time
|
||||
|
||||
from api.metrics import record_db_query
|
||||
from models.decision import ListingDecision
|
||||
from sqlalchemy import Engine
|
||||
from sqlmodel import Session, select
|
||||
|
|
@ -19,6 +21,7 @@ class DecisionRepository:
|
|||
decision: str,
|
||||
) -> ListingDecision:
|
||||
"""Create or update a decision. Uses dialect-specific upsert."""
|
||||
t0 = time.monotonic()
|
||||
with Session(self.engine) as session:
|
||||
now = datetime.utcnow()
|
||||
values = {
|
||||
|
|
@ -58,14 +61,18 @@ class DecisionRepository:
|
|||
)
|
||||
).first()
|
||||
assert result is not None
|
||||
return result
|
||||
record_db_query("upsert_decision", "decision", time.monotonic() - t0)
|
||||
return result
|
||||
|
||||
def get_decisions_for_user(self, user_id: int) -> list[ListingDecision]:
|
||||
t0 = time.monotonic()
|
||||
with Session(self.engine) as session:
|
||||
statement = select(ListingDecision).where(
|
||||
ListingDecision.user_id == user_id
|
||||
)
|
||||
return list(session.exec(statement).all())
|
||||
results = list(session.exec(statement).all())
|
||||
record_db_query("get_decisions_for_user", "decision", time.monotonic() - t0, len(results))
|
||||
return results
|
||||
|
||||
def delete_decision(
|
||||
self,
|
||||
|
|
@ -92,13 +99,16 @@ class DecisionRepository:
|
|||
user_id: int,
|
||||
listing_type: str,
|
||||
) -> set[int]:
|
||||
t0 = time.monotonic()
|
||||
with Session(self.engine) as session:
|
||||
statement = select(ListingDecision.listing_id).where(
|
||||
ListingDecision.user_id == user_id,
|
||||
ListingDecision.listing_type == listing_type,
|
||||
ListingDecision.decision == "disliked",
|
||||
)
|
||||
return {row for row in session.exec(statement).all()}
|
||||
ids = {row for row in session.exec(statement).all()}
|
||||
record_db_query("get_disliked_listing_ids", "decision", time.monotonic() - t0, len(ids))
|
||||
return ids
|
||||
|
||||
def get_liked_listing_ids(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import time
|
||||
from api.metrics import record_db_query
|
||||
from models.listing import ListingType
|
||||
from models.poi import PointOfInterest
|
||||
from models.poi_distance import POIDistance
|
||||
|
|
@ -12,11 +14,14 @@ class POIRepository:
|
|||
self.engine = engine
|
||||
|
||||
def get_pois_for_user(self, user_id: int) -> list[PointOfInterest]:
|
||||
t0 = time.monotonic()
|
||||
with Session(self.engine) as session:
|
||||
statement = select(PointOfInterest).where(
|
||||
PointOfInterest.user_id == user_id
|
||||
)
|
||||
return list(session.exec(statement).all())
|
||||
results = list(session.exec(statement).all())
|
||||
record_db_query("get_pois_for_user", "poi", time.monotonic() - t0, len(results))
|
||||
return results
|
||||
|
||||
def get_poi_by_id(self, poi_id: int) -> PointOfInterest | None:
|
||||
with Session(self.engine) as session:
|
||||
|
|
@ -55,6 +60,7 @@ class POIRepository:
|
|||
"""Insert or update POI distances, handling duplicate unique constraints."""
|
||||
if not distances:
|
||||
return
|
||||
t0 = time.monotonic()
|
||||
with Session(self.engine) as session:
|
||||
dialect = self.engine.dialect.name
|
||||
for dist in distances:
|
||||
|
|
@ -88,6 +94,7 @@ class POIRepository:
|
|||
)
|
||||
session.execute(stmt)
|
||||
session.commit()
|
||||
record_db_query("upsert_distances", "poi_distance", time.monotonic() - t0, len(distances))
|
||||
|
||||
def get_distances_for_listings(
|
||||
self,
|
||||
|
|
@ -95,6 +102,7 @@ class POIRepository:
|
|||
listing_type: ListingType,
|
||||
user_id: int,
|
||||
) -> list[POIDistance]:
|
||||
t0 = time.monotonic()
|
||||
with Session(self.engine) as session:
|
||||
# Join with POI to filter by user
|
||||
statement = (
|
||||
|
|
@ -106,7 +114,9 @@ class POIRepository:
|
|||
PointOfInterest.user_id == user_id,
|
||||
)
|
||||
)
|
||||
return list(session.exec(statement).all())
|
||||
results = list(session.exec(statement).all())
|
||||
record_db_query("get_distances_for_listings", "poi_distance", time.monotonic() - t0, len(results))
|
||||
return results
|
||||
|
||||
def get_distances_for_poi(self, poi_id: int) -> list[POIDistance]:
|
||||
with Session(self.engine) as session:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue