fix: use cached_slide_count instead of from_cache boolean for accurate telemetry
Browse filesReplace boolean from_cache flag with integer cached_slide_count to properly
track cache hits at the slide level instead of batch level. This fixes
incorrect average duration calculation for mixed batches (some cached,
some fresh slides).
Previous behavior:
- Mixed batch (3 cached, 7 fresh) with 70s duration was excluded from
average if any slide was cached
New behavior:
- Only fully cached batches (all slides cached) are excluded from average
- Mixed batches are correctly included in average duration
- Reports now show total cached slides and cache hit rate percentage
Changes:
- UsageEvent: from_cache (bool) -> cached_slide_count (int)
- TelemetryTracker: Updated session metrics to check if all slides cached
- UI: Track cached_slides and fresh_slides counters separately
- Reports: Show 'Cached slides: X' and 'Cache hit rate: Y%'
- Tests: Updated all tests to use new cached_slide_count field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- scripts/telemetry_report.py +25 -8
- src/mosaic/telemetry/events.py +3 -1
- src/mosaic/telemetry/tracker.py +10 -5
- src/mosaic/ui/app.py +5 -3
- tests/telemetry/test_events.py +10 -10
- tests/telemetry/test_report.py +15 -8
- tests/telemetry/test_tracker.py +15 -15
|
@@ -205,10 +205,19 @@ def generate_text_report(telemetry_dir: Path, date: Optional[str] = None) -> str
|
|
| 205 |
)
|
| 206 |
|
| 207 |
# Cache hit tracking
|
| 208 |
-
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
|
| 211 |
-
# Calculate average duration (excluding cached analyses)
|
| 212 |
durations = [
|
| 213 |
c.get("duration_sec", 0) for c in fresh_analyses if c.get("duration_sec")
|
| 214 |
]
|
|
@@ -219,8 +228,10 @@ def generate_text_report(telemetry_dir: Path, date: Optional[str] = None) -> str
|
|
| 219 |
lines.append(f"Analyses completed: {len(completes)}")
|
| 220 |
lines.append(f"Successful analyses: {len(successful)}")
|
| 221 |
lines.append(f"Total slides processed: {total_slides}")
|
| 222 |
-
if
|
| 223 |
-
lines.append(f"Cached
|
|
|
|
|
|
|
| 224 |
lines.append(f"Unique sessions: {unique_sessions}")
|
| 225 |
if avg_duration > 0:
|
| 226 |
lines.append(f"Average analysis duration: {avg_duration:.1f}s")
|
|
@@ -438,7 +449,9 @@ def generate_html_report(telemetry_dir: Path, date: Optional[str] = None) -> str
|
|
| 438 |
)
|
| 439 |
|
| 440 |
# Cache hit tracking
|
| 441 |
-
|
|
|
|
|
|
|
| 442 |
|
| 443 |
html.append("<h2>Usage Summary</h2>")
|
| 444 |
html.append("<table>")
|
|
@@ -448,9 +461,13 @@ def generate_html_report(telemetry_dir: Path, date: Optional[str] = None) -> str
|
|
| 448 |
f"<tr><td>Successful analyses</td><td class='success'>{len(successful)}</td></tr>"
|
| 449 |
)
|
| 450 |
html.append(f"<tr><td>Total slides</td><td>{total_slides}</td></tr>")
|
| 451 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
html.append(
|
| 453 |
-
f"<tr><td>
|
| 454 |
)
|
| 455 |
html.append(f"<tr><td>Unique sessions</td><td>{unique_sessions}</td></tr>")
|
| 456 |
html.append("</table>")
|
|
|
|
| 205 |
)
|
| 206 |
|
| 207 |
# Cache hit tracking
|
| 208 |
+
total_cached_slides = sum(
|
| 209 |
+
c.get("cached_slide_count", 0) for c in completes if c.get("cached_slide_count")
|
| 210 |
+
)
|
| 211 |
+
fully_cached_analyses = [
|
| 212 |
+
c for c in completes
|
| 213 |
+
if c.get("cached_slide_count") and c.get("cached_slide_count") == c.get("slide_count", 0)
|
| 214 |
+
]
|
| 215 |
+
fresh_analyses = [
|
| 216 |
+
c for c in completes
|
| 217 |
+
if not c.get("cached_slide_count") or c.get("cached_slide_count") < c.get("slide_count", 0)
|
| 218 |
+
]
|
| 219 |
|
| 220 |
+
# Calculate average duration (excluding fully cached analyses)
|
| 221 |
durations = [
|
| 222 |
c.get("duration_sec", 0) for c in fresh_analyses if c.get("duration_sec")
|
| 223 |
]
|
|
|
|
| 228 |
lines.append(f"Analyses completed: {len(completes)}")
|
| 229 |
lines.append(f"Successful analyses: {len(successful)}")
|
| 230 |
lines.append(f"Total slides processed: {total_slides}")
|
| 231 |
+
if total_cached_slides > 0:
|
| 232 |
+
lines.append(f"Cached slides: {total_cached_slides}")
|
| 233 |
+
cache_rate = (total_cached_slides / total_slides * 100) if total_slides > 0 else 0
|
| 234 |
+
lines.append(f"Cache hit rate: {cache_rate:.1f}%")
|
| 235 |
lines.append(f"Unique sessions: {unique_sessions}")
|
| 236 |
if avg_duration > 0:
|
| 237 |
lines.append(f"Average analysis duration: {avg_duration:.1f}s")
|
|
|
|
| 449 |
)
|
| 450 |
|
| 451 |
# Cache hit tracking
|
| 452 |
+
total_cached_slides = sum(
|
| 453 |
+
c.get("cached_slide_count", 0) for c in completes if c.get("cached_slide_count")
|
| 454 |
+
)
|
| 455 |
|
| 456 |
html.append("<h2>Usage Summary</h2>")
|
| 457 |
html.append("<table>")
|
|
|
|
| 461 |
f"<tr><td>Successful analyses</td><td class='success'>{len(successful)}</td></tr>"
|
| 462 |
)
|
| 463 |
html.append(f"<tr><td>Total slides</td><td>{total_slides}</td></tr>")
|
| 464 |
+
if total_cached_slides > 0:
|
| 465 |
+
html.append(
|
| 466 |
+
f"<tr><td>Cached slides</td><td>{total_cached_slides}</td></tr>"
|
| 467 |
+
)
|
| 468 |
+
cache_rate = (total_cached_slides / total_slides * 100) if total_slides > 0 else 0
|
| 469 |
html.append(
|
| 470 |
+
f"<tr><td>Cache hit rate</td><td>{cache_rate:.1f}%</td></tr>"
|
| 471 |
)
|
| 472 |
html.append(f"<tr><td>Unique sessions</td><td>{unique_sessions}</td></tr>")
|
| 473 |
html.append("</table>")
|
|
@@ -65,7 +65,9 @@ class UsageEvent:
|
|
| 65 |
gpu_type: Optional[str] = None # From hardware.GPU_TYPE
|
| 66 |
duration_sec: Optional[float] = None # On completion only
|
| 67 |
success: Optional[bool] = None # On completion only
|
| 68 |
-
|
|
|
|
|
|
|
| 69 |
is_logged_in: Optional[bool] = None # True if HF user logged in
|
| 70 |
hf_username: Optional[str] = None # Raw HF username (HF Spaces only)
|
| 71 |
event_id: str = field(default_factory=_generate_event_id)
|
|
|
|
| 65 |
gpu_type: Optional[str] = None # From hardware.GPU_TYPE
|
| 66 |
duration_sec: Optional[float] = None # On completion only
|
| 67 |
success: Optional[bool] = None # On completion only
|
| 68 |
+
cached_slide_count: Optional[int] = (
|
| 69 |
+
None # Number of cached slides (completion only)
|
| 70 |
+
)
|
| 71 |
is_logged_in: Optional[bool] = None # True if HF user logged in
|
| 72 |
hf_username: Optional[str] = None # Raw HF username (HF Spaces only)
|
| 73 |
event_id: str = field(default_factory=_generate_event_id)
|
|
@@ -215,7 +215,7 @@ class TelemetryTracker:
|
|
| 215 |
gpu_type: Optional[str] = None,
|
| 216 |
duration_sec: Optional[float] = None,
|
| 217 |
success: Optional[bool] = None,
|
| 218 |
-
|
| 219 |
is_logged_in: Optional[bool] = None,
|
| 220 |
hf_username: Optional[str] = None,
|
| 221 |
) -> None:
|
|
@@ -232,7 +232,7 @@ class TelemetryTracker:
|
|
| 232 |
gpu_type: GPU type string
|
| 233 |
duration_sec: Analysis duration (for analysis_complete only)
|
| 234 |
success: Whether analysis succeeded (for analysis_complete only)
|
| 235 |
-
|
| 236 |
is_logged_in: True if HF user logged in
|
| 237 |
hf_username: HF username (HF Spaces only)
|
| 238 |
"""
|
|
@@ -250,7 +250,7 @@ class TelemetryTracker:
|
|
| 250 |
gpu_type=gpu_type,
|
| 251 |
duration_sec=duration_sec,
|
| 252 |
success=success,
|
| 253 |
-
|
| 254 |
is_logged_in=is_logged_in,
|
| 255 |
hf_username=hf_username,
|
| 256 |
)
|
|
@@ -260,8 +260,13 @@ class TelemetryTracker:
|
|
| 260 |
if event_type == "analysis_complete" and duration_sec is not None:
|
| 261 |
with self._session_lock:
|
| 262 |
self._analysis_count += 1
|
| 263 |
-
# Don't count duration if
|
| 264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
self._analysis_time_sec += duration_sec
|
| 266 |
|
| 267 |
# =========================================================================
|
|
|
|
| 215 |
gpu_type: Optional[str] = None,
|
| 216 |
duration_sec: Optional[float] = None,
|
| 217 |
success: Optional[bool] = None,
|
| 218 |
+
cached_slide_count: Optional[int] = None,
|
| 219 |
is_logged_in: Optional[bool] = None,
|
| 220 |
hf_username: Optional[str] = None,
|
| 221 |
) -> None:
|
|
|
|
| 232 |
gpu_type: GPU type string
|
| 233 |
duration_sec: Analysis duration (for analysis_complete only)
|
| 234 |
success: Whether analysis succeeded (for analysis_complete only)
|
| 235 |
+
cached_slide_count: Number of slides served from cache (for analysis_complete only)
|
| 236 |
is_logged_in: True if HF user logged in
|
| 237 |
hf_username: HF username (HF Spaces only)
|
| 238 |
"""
|
|
|
|
| 250 |
gpu_type=gpu_type,
|
| 251 |
duration_sec=duration_sec,
|
| 252 |
success=success,
|
| 253 |
+
cached_slide_count=cached_slide_count,
|
| 254 |
is_logged_in=is_logged_in,
|
| 255 |
hf_username=hf_username,
|
| 256 |
)
|
|
|
|
| 260 |
if event_type == "analysis_complete" and duration_sec is not None:
|
| 261 |
with self._session_lock:
|
| 262 |
self._analysis_count += 1
|
| 263 |
+
# Don't count duration if ALL slides were served from cache
|
| 264 |
+
is_fully_cached = (
|
| 265 |
+
cached_slide_count is not None
|
| 266 |
+
and slide_count > 0
|
| 267 |
+
and cached_slide_count == slide_count
|
| 268 |
+
)
|
| 269 |
+
if not is_fully_cached:
|
| 270 |
self._analysis_time_sec += duration_sec
|
| 271 |
|
| 272 |
# =========================================================================
|
|
@@ -289,7 +289,8 @@ def analyze_slides(
|
|
| 289 |
all_slide_masks = []
|
| 290 |
all_aeon_results = []
|
| 291 |
all_paladin_results = []
|
| 292 |
-
|
|
|
|
| 293 |
|
| 294 |
# Log analysis start event
|
| 295 |
tracker.log_usage_event(
|
|
@@ -402,7 +403,7 @@ def analyze_slides(
|
|
| 402 |
cached = load_analysis_results(file_uuid, settings_hash)
|
| 403 |
if cached is not None:
|
| 404 |
slide_mask, aeon_results, paladin_results = cached
|
| 405 |
-
|
| 406 |
logger.info(
|
| 407 |
f"Using cached results for {file_uuid}/{settings_hash}"
|
| 408 |
)
|
|
@@ -416,6 +417,7 @@ def analyze_slides(
|
|
| 416 |
|
| 417 |
# If no cached results, run analysis
|
| 418 |
if slide_mask is None or aeon_results is None or paladin_results is None:
|
|
|
|
| 419 |
# Lazy model loading: load on first cache miss
|
| 420 |
if model_cache is None:
|
| 421 |
if IS_T4_GPU:
|
|
@@ -514,7 +516,7 @@ def analyze_slides(
|
|
| 514 |
slide_count=len(slides),
|
| 515 |
duration_sec=duration_sec,
|
| 516 |
success=True,
|
| 517 |
-
|
| 518 |
gpu_type=GPU_TYPE,
|
| 519 |
is_logged_in=user_info.is_logged_in,
|
| 520 |
hf_username=user_info.username,
|
|
|
|
| 289 |
all_slide_masks = []
|
| 290 |
all_aeon_results = []
|
| 291 |
all_paladin_results = []
|
| 292 |
+
cached_slides = 0
|
| 293 |
+
fresh_slides = 0
|
| 294 |
|
| 295 |
# Log analysis start event
|
| 296 |
tracker.log_usage_event(
|
|
|
|
| 403 |
cached = load_analysis_results(file_uuid, settings_hash)
|
| 404 |
if cached is not None:
|
| 405 |
slide_mask, aeon_results, paladin_results = cached
|
| 406 |
+
cached_slides += 1
|
| 407 |
logger.info(
|
| 408 |
f"Using cached results for {file_uuid}/{settings_hash}"
|
| 409 |
)
|
|
|
|
| 417 |
|
| 418 |
# If no cached results, run analysis
|
| 419 |
if slide_mask is None or aeon_results is None or paladin_results is None:
|
| 420 |
+
fresh_slides += 1
|
| 421 |
# Lazy model loading: load on first cache miss
|
| 422 |
if model_cache is None:
|
| 423 |
if IS_T4_GPU:
|
|
|
|
| 516 |
slide_count=len(slides),
|
| 517 |
duration_sec=duration_sec,
|
| 518 |
success=True,
|
| 519 |
+
cached_slide_count=cached_slides if cached_slides > 0 else None,
|
| 520 |
gpu_type=GPU_TYPE,
|
| 521 |
is_logged_in=user_info.is_logged_in,
|
| 522 |
hf_username=user_info.username,
|
|
@@ -108,24 +108,24 @@ class TestUsageEvent:
|
|
| 108 |
assert data["slide_count"] == 3
|
| 109 |
assert data["session_hash"] is None
|
| 110 |
|
| 111 |
-
def
|
| 112 |
-
"""Test
|
| 113 |
event = UsageEvent(
|
| 114 |
event_type="analysis_complete",
|
| 115 |
analysis_id="test-123",
|
| 116 |
session_hash=None,
|
| 117 |
-
slide_count=
|
| 118 |
duration_sec=2.0,
|
| 119 |
success=True,
|
| 120 |
-
|
| 121 |
)
|
| 122 |
|
| 123 |
-
assert event.
|
| 124 |
data = event.to_dict()
|
| 125 |
-
assert data["
|
| 126 |
|
| 127 |
-
def
|
| 128 |
-
"""Test that
|
| 129 |
event = UsageEvent(
|
| 130 |
event_type="analysis_complete",
|
| 131 |
analysis_id="test-123",
|
|
@@ -135,9 +135,9 @@ class TestUsageEvent:
|
|
| 135 |
success=True,
|
| 136 |
)
|
| 137 |
|
| 138 |
-
assert event.
|
| 139 |
data = event.to_dict()
|
| 140 |
-
assert data["
|
| 141 |
|
| 142 |
def test_user_info_fields(self):
|
| 143 |
"""Test is_logged_in and hf_username fields in UsageEvent."""
|
|
|
|
| 108 |
assert data["slide_count"] == 3
|
| 109 |
assert data["session_hash"] is None
|
| 110 |
|
| 111 |
+
def test_cached_slide_count(self):
|
| 112 |
+
"""Test cached_slide_count field on analysis_complete event."""
|
| 113 |
event = UsageEvent(
|
| 114 |
event_type="analysis_complete",
|
| 115 |
analysis_id="test-123",
|
| 116 |
session_hash=None,
|
| 117 |
+
slide_count=10,
|
| 118 |
duration_sec=2.0,
|
| 119 |
success=True,
|
| 120 |
+
cached_slide_count=3,
|
| 121 |
)
|
| 122 |
|
| 123 |
+
assert event.cached_slide_count == 3
|
| 124 |
data = event.to_dict()
|
| 125 |
+
assert data["cached_slide_count"] == 3
|
| 126 |
|
| 127 |
+
def test_cached_slide_count_default_none(self):
|
| 128 |
+
"""Test that cached_slide_count defaults to None."""
|
| 129 |
event = UsageEvent(
|
| 130 |
event_type="analysis_complete",
|
| 131 |
analysis_id="test-123",
|
|
|
|
| 135 |
success=True,
|
| 136 |
)
|
| 137 |
|
| 138 |
+
assert event.cached_slide_count is None
|
| 139 |
data = event.to_dict()
|
| 140 |
+
assert data["cached_slide_count"] is None
|
| 141 |
|
| 142 |
def test_user_info_fields(self):
|
| 143 |
"""Test is_logged_in and hf_username fields in UsageEvent."""
|
|
@@ -288,11 +288,12 @@ class TestGenerateTextReport:
|
|
| 288 |
assert "for 2026-01-20" in report
|
| 289 |
|
| 290 |
def test_report_with_cached_analysis(self, tmp_path):
|
| 291 |
-
"""Test report shows cached
|
| 292 |
daily_dir = tmp_path / "daily"
|
| 293 |
daily_dir.mkdir()
|
| 294 |
|
| 295 |
usage = [
|
|
|
|
| 296 |
{
|
| 297 |
"event_type": "analysis_start",
|
| 298 |
"slide_count": 1,
|
|
@@ -305,8 +306,9 @@ class TestGenerateTextReport:
|
|
| 305 |
"success": True,
|
| 306 |
"duration_sec": 2.0,
|
| 307 |
"slide_count": 1,
|
| 308 |
-
"
|
| 309 |
},
|
|
|
|
| 310 |
{
|
| 311 |
"event_type": "analysis_start",
|
| 312 |
"slide_count": 1,
|
|
@@ -319,6 +321,7 @@ class TestGenerateTextReport:
|
|
| 319 |
"success": True,
|
| 320 |
"duration_sec": 120.0,
|
| 321 |
"slide_count": 1,
|
|
|
|
| 322 |
},
|
| 323 |
]
|
| 324 |
|
|
@@ -328,8 +331,10 @@ class TestGenerateTextReport:
|
|
| 328 |
f.write(json.dumps(event) + "\n")
|
| 329 |
|
| 330 |
report = generate_text_report(tmp_path)
|
| 331 |
-
|
| 332 |
-
|
|
|
|
|
|
|
| 333 |
# only include the fresh one (120s)
|
| 334 |
assert "Average analysis duration: 120.0s" in report
|
| 335 |
|
|
@@ -360,7 +365,8 @@ class TestGenerateTextReport:
|
|
| 360 |
f.write(json.dumps(event) + "\n")
|
| 361 |
|
| 362 |
report = generate_text_report(tmp_path)
|
| 363 |
-
assert "Cached
|
|
|
|
| 364 |
|
| 365 |
def test_report_with_resources(self, tmp_path):
|
| 366 |
"""Test report with resource data."""
|
|
@@ -423,7 +429,7 @@ class TestGenerateHtmlReport:
|
|
| 423 |
assert "<td>1</td>" in report # 1 analysis started
|
| 424 |
|
| 425 |
def test_html_with_cached_analysis(self, tmp_path):
|
| 426 |
-
"""Test HTML report shows cached
|
| 427 |
daily_dir = tmp_path / "daily"
|
| 428 |
daily_dir.mkdir()
|
| 429 |
|
|
@@ -438,7 +444,7 @@ class TestGenerateHtmlReport:
|
|
| 438 |
"success": True,
|
| 439 |
"duration_sec": 2.0,
|
| 440 |
"slide_count": 1,
|
| 441 |
-
"
|
| 442 |
},
|
| 443 |
]
|
| 444 |
|
|
@@ -448,7 +454,8 @@ class TestGenerateHtmlReport:
|
|
| 448 |
f.write(json.dumps(event) + "\n")
|
| 449 |
|
| 450 |
report = generate_html_report(tmp_path)
|
| 451 |
-
assert "Cached
|
|
|
|
| 452 |
|
| 453 |
def test_html_with_failures(self, tmp_path):
|
| 454 |
"""Test HTML report with failures."""
|
|
|
|
| 288 |
assert "for 2026-01-20" in report
|
| 289 |
|
| 290 |
def test_report_with_cached_analysis(self, tmp_path):
|
| 291 |
+
"""Test report shows cached slide count and cache hit rate."""
|
| 292 |
daily_dir = tmp_path / "daily"
|
| 293 |
daily_dir.mkdir()
|
| 294 |
|
| 295 |
usage = [
|
| 296 |
+
# Batch 1: Fully cached (1/1 slides cached)
|
| 297 |
{
|
| 298 |
"event_type": "analysis_start",
|
| 299 |
"slide_count": 1,
|
|
|
|
| 306 |
"success": True,
|
| 307 |
"duration_sec": 2.0,
|
| 308 |
"slide_count": 1,
|
| 309 |
+
"cached_slide_count": 1, # Fully cached
|
| 310 |
},
|
| 311 |
+
# Batch 2: Fresh (0/1 slides cached)
|
| 312 |
{
|
| 313 |
"event_type": "analysis_start",
|
| 314 |
"slide_count": 1,
|
|
|
|
| 321 |
"success": True,
|
| 322 |
"duration_sec": 120.0,
|
| 323 |
"slide_count": 1,
|
| 324 |
+
# No cached_slide_count = 0 cached
|
| 325 |
},
|
| 326 |
]
|
| 327 |
|
|
|
|
| 331 |
f.write(json.dumps(event) + "\n")
|
| 332 |
|
| 333 |
report = generate_text_report(tmp_path)
|
| 334 |
+
# Should show 1 cached slide out of 2 total
|
| 335 |
+
assert "Cached slides: 1" in report
|
| 336 |
+
assert "Cache hit rate: 50.0%" in report
|
| 337 |
+
# Average duration should exclude fully cached batch (2s),
|
| 338 |
# only include the fresh one (120s)
|
| 339 |
assert "Average analysis duration: 120.0s" in report
|
| 340 |
|
|
|
|
| 365 |
f.write(json.dumps(event) + "\n")
|
| 366 |
|
| 367 |
report = generate_text_report(tmp_path)
|
| 368 |
+
assert "Cached slides" not in report
|
| 369 |
+
assert "Cache hit rate" not in report
|
| 370 |
|
| 371 |
def test_report_with_resources(self, tmp_path):
|
| 372 |
"""Test report with resource data."""
|
|
|
|
| 429 |
assert "<td>1</td>" in report # 1 analysis started
|
| 430 |
|
| 431 |
def test_html_with_cached_analysis(self, tmp_path):
|
| 432 |
+
"""Test HTML report shows cached slide count and hit rate."""
|
| 433 |
daily_dir = tmp_path / "daily"
|
| 434 |
daily_dir.mkdir()
|
| 435 |
|
|
|
|
| 444 |
"success": True,
|
| 445 |
"duration_sec": 2.0,
|
| 446 |
"slide_count": 1,
|
| 447 |
+
"cached_slide_count": 1,
|
| 448 |
},
|
| 449 |
]
|
| 450 |
|
|
|
|
| 454 |
f.write(json.dumps(event) + "\n")
|
| 455 |
|
| 456 |
report = generate_html_report(tmp_path)
|
| 457 |
+
assert "Cached slides" in report
|
| 458 |
+
assert "Cache hit rate" in report
|
| 459 |
|
| 460 |
def test_html_with_failures(self, tmp_path):
|
| 461 |
"""Test HTML report with failures."""
|
|
@@ -196,49 +196,49 @@ class TestUsageEvents:
|
|
| 196 |
assert metrics["analysis_count"] == 2
|
| 197 |
assert metrics["analysis_time_sec"] == 105.0
|
| 198 |
|
| 199 |
-
def
|
| 200 |
-
"""Test that
|
| 201 |
tracker.log_usage_event(
|
| 202 |
event_type="analysis_complete",
|
| 203 |
analysis_id="test-cache",
|
| 204 |
-
slide_count=
|
| 205 |
-
duration_sec=
|
| 206 |
success=True,
|
| 207 |
-
|
| 208 |
)
|
| 209 |
|
| 210 |
usage_files = list((temp_dir / "daily").glob("usage_*.jsonl"))
|
| 211 |
with open(usage_files[0]) as f:
|
| 212 |
event = json.loads(f.read().strip())
|
| 213 |
|
| 214 |
-
assert event["
|
| 215 |
|
| 216 |
-
def
|
| 217 |
-
"""Test that cached analyses don't count toward analysis_time_sec."""
|
| 218 |
tracker.log_app_start()
|
| 219 |
tracker.log_usage_event(
|
| 220 |
event_type="analysis_complete",
|
| 221 |
analysis_id="test-cache",
|
| 222 |
-
slide_count=
|
| 223 |
duration_sec=2.0,
|
| 224 |
success=True,
|
| 225 |
-
|
| 226 |
)
|
| 227 |
|
| 228 |
metrics = tracker._get_session_metrics()
|
| 229 |
assert metrics["analysis_count"] == 1
|
| 230 |
assert metrics["analysis_time_sec"] == 0.0
|
| 231 |
|
| 232 |
-
def
|
| 233 |
-
"""Test that
|
| 234 |
tracker.log_app_start()
|
| 235 |
tracker.log_usage_event(
|
| 236 |
event_type="analysis_complete",
|
| 237 |
-
analysis_id="test-
|
| 238 |
-
slide_count=
|
| 239 |
duration_sec=60.0,
|
| 240 |
success=True,
|
| 241 |
-
|
| 242 |
)
|
| 243 |
|
| 244 |
metrics = tracker._get_session_metrics()
|
|
|
|
| 196 |
assert metrics["analysis_count"] == 2
|
| 197 |
assert metrics["analysis_time_sec"] == 105.0
|
| 198 |
|
| 199 |
+
def test_log_usage_event_with_cached_slides(self, tracker, temp_dir):
|
| 200 |
+
"""Test that cached_slide_count is persisted in usage events."""
|
| 201 |
tracker.log_usage_event(
|
| 202 |
event_type="analysis_complete",
|
| 203 |
analysis_id="test-cache",
|
| 204 |
+
slide_count=10,
|
| 205 |
+
duration_sec=50.0,
|
| 206 |
success=True,
|
| 207 |
+
cached_slide_count=3,
|
| 208 |
)
|
| 209 |
|
| 210 |
usage_files = list((temp_dir / "daily").glob("usage_*.jsonl"))
|
| 211 |
with open(usage_files[0]) as f:
|
| 212 |
event = json.loads(f.read().strip())
|
| 213 |
|
| 214 |
+
assert event["cached_slide_count"] == 3
|
| 215 |
|
| 216 |
+
def test_fully_cached_analysis_excludes_duration_from_metrics(self, tracker):
|
| 217 |
+
"""Test that fully cached analyses don't count toward analysis_time_sec."""
|
| 218 |
tracker.log_app_start()
|
| 219 |
tracker.log_usage_event(
|
| 220 |
event_type="analysis_complete",
|
| 221 |
analysis_id="test-cache",
|
| 222 |
+
slide_count=5,
|
| 223 |
duration_sec=2.0,
|
| 224 |
success=True,
|
| 225 |
+
cached_slide_count=5, # All slides cached
|
| 226 |
)
|
| 227 |
|
| 228 |
metrics = tracker._get_session_metrics()
|
| 229 |
assert metrics["analysis_count"] == 1
|
| 230 |
assert metrics["analysis_time_sec"] == 0.0
|
| 231 |
|
| 232 |
+
def test_mixed_cache_analysis_includes_duration_in_metrics(self, tracker):
|
| 233 |
+
"""Test that mixed analyses (some cached) count toward analysis_time_sec."""
|
| 234 |
tracker.log_app_start()
|
| 235 |
tracker.log_usage_event(
|
| 236 |
event_type="analysis_complete",
|
| 237 |
+
analysis_id="test-mixed",
|
| 238 |
+
slide_count=10,
|
| 239 |
duration_sec=60.0,
|
| 240 |
success=True,
|
| 241 |
+
cached_slide_count=3, # Only 3/10 cached
|
| 242 |
)
|
| 243 |
|
| 244 |
metrics = tracker._get_session_metrics()
|