Expand dashboard analytics and document production implementation

This commit is contained in:
maxwes08
2026-11-03 14:49:01 +01:00
parent 70b94f8672
commit aa0028e7e0
36 changed files with 2212 additions and 76 deletions

View File

@@ -11,6 +11,10 @@ from database import connect
from metrics_api import engagement, monetization, gamepasses
from sample_api import ApiError, dispatch, player_values, stage_funnel, player_detail, sessions
from psycopg.types.json import Jsonb
from player_records import record_page,player_inventory
from prompt_metrics import prompt_analytics
from health_reports import window_metrics,reports_for_game,change
from data_quality import quality_log
class MetricDefinitions(unittest.TestCase):
@@ -115,6 +119,99 @@ class MetricDefinitions(unittest.TestCase):
self.assertEqual(sum(s['joinAvailable'] for s in records),1)
self.assertTrue(all(not s['joinAvailable'] for s in records if s['ended_at']))
def test_record_pages_newest_first_ties_filters_and_game_scope(self):
pid=self.players[0]
for i in range(25):
self.conn.execute("INSERT INTO events(game_id,player_id,name,occurred_at) VALUES(%s,%s,%s,%s)",(self.game,pid,'clicked' if i%2 else 'opened',self.yesterday))
self.conn.execute('INSERT INTO player_snapshots(game_id,player_id,captured_at,data) VALUES(%s,%s,%s,%s)',(self.game,pid,self.yesterday+timedelta(seconds=i),Jsonb({'cash':i})))
for kind in ['events','snapshots']:
first=record_page(self.conn,self.game,pid,kind,{})
second=record_page(self.conn,self.game,pid,kind,{'page':['2']})
self.assertEqual(first['pagination']['total'],25)
self.assertEqual(len(first['records']),20)
self.assertEqual(len(second['records']),5)
self.assertFalse({r['id'] for r in first['records']} & {r['id'] for r in second['records']})
self.assertGreater(first['records'][0]['id'],second['records'][0]['id'])
filtered=record_page(self.conn,self.game,pid,'events',{'event':['clicked']})
self.assertEqual(filtered['pagination']['total'],12)
self.assertTrue(all(r['name']=='clicked' for r in filtered['records']))
sales=record_page(self.conn,self.game,pid,'purchases',{'product':[str(self.products[1])]})
self.assertEqual(sales['pagination']['total'],0)
with self.assertRaises(ApiError): record_page(self.conn,self.game+999,pid,'events',{})
def test_prompt_timings_conversion_and_unresolved(self):
pid=self.players[0]
for i,(outcome,seconds) in enumerate([('bought',10),('bought',30),('closed',5),(None,None)]):
self.conn.execute('''INSERT INTO interaction_events(game_id,kind,report_key,player_id,product_id,started_at,ended_at,outcome,trigger_source)
VALUES(%s,'product_prompt',%s,%s,%s,%s,%s,%s,%s)''',(self.game,str(i),pid,self.products[0],self.yesterday,
self.yesterday+timedelta(seconds=seconds) if seconds else None,outcome,['player','game','game','unknown'][i]))
data=prompt_analytics(self.conn,self.game,2)
row=next(r for r in data['products'] if r['id']==self.products[0])
self.assertAlmostEqual(row['conversion'],200/3)
self.assertEqual(row['buy_time'],20)
self.assertEqual(row['close_time'],5)
self.assertEqual(row['unresolved'],0)
self.assertEqual(data['metrics']['prompts'],3)
sources={r['source']:r for r in data['sourceComparison']}
self.assertEqual(sources['player']['conversion'],100)
self.assertEqual(sources['game']['conversion'],50)
self.assertNotIn('unknown',sources)
filtered=prompt_analytics(self.conn,self.game,2,'game')
self.assertEqual(filtered['metrics']['prompts'],2)
self.assertEqual(filtered['metrics']['buy_time'],30)
self.assertEqual(sum(r['prompts'] for r in filtered['products']),2)
self.assertEqual(sum(r['prompts'] for r in filtered['sourceComparison']),3)
log=quality_log(self.conn,self.game,{})
self.assertEqual(log['pagination']['total'],2)
self.assertEqual({r['issue'] for r in log['records']},{'prompt_source','prompt_outcome'})
self.assertEqual(quality_log(self.conn,self.game,{'issue':['prompt_outcome']})['pagination']['total'],1)
self.assertEqual(self.conn.execute('SELECT count(*) n FROM interaction_events WHERE game_id=%s',(self.game,)).fetchone()['n'],4)
with self.assertRaises(ApiError): prompt_analytics(self.conn,self.game,2,'unknown')
with self.assertRaises(ApiError): prompt_analytics(self.conn,self.game,2,'invalid')
with self.assertRaises(psycopg.errors.CheckViolation):
with self.conn.transaction():
self.conn.execute("UPDATE interaction_events SET trigger_source='invalid' WHERE game_id=%s",(self.game,))
def test_inventory_orders_purchase_dates_and_aggregates_quantities(self):
pid=self.players[0]
for n,at in [(1,self.today),(2,self.yesterday),(3,None)]:
gp=self.conn.execute('INSERT INTO gamepasses(game_id,roblox_gamepass_id,name) VALUES(%s,%s,%s) RETURNING id',(self.game,n,str(n))).fetchone()['id']
self.conn.execute('INSERT INTO gamepass_ownership(game_id,gamepass_id,player_id,is_owned,checked_at,purchased_at) VALUES(%s,%s,%s,true,now(),%s)',(self.game,gp,pid,at))
inv=player_inventory(self.conn,self.game,pid)
self.assertEqual([p['name'] for p in inv['ownedGamepasses']],['2','1','3'])
self.assertEqual(inv['purchasedProducts'][0]['quantity'],2)
self.assertEqual(inv['purchasedProducts'][0]['robux'],200)
def test_daily_health_report_complete_windows_and_cache(self):
today=self.today.date()
for day in [self.yesterday,self.yesterday-timedelta(days=1)]:
for hour,value in enumerate([0,4,None]):
self.conn.execute("INSERT INTO game_metrics(game_id,category,name,recorded_at,value) VALUES(%s,'ccu','concurrent_players',%s,%s)",(self.game,day+timedelta(hours=hour),value))
metrics=window_metrics(self.conn,self.game,today-timedelta(days=1),today)
self.assertEqual(metrics['sessions'],2)
self.assertEqual(metrics['average_session'],210)
self.assertEqual(metrics['total_playtime'],420)
self.assertEqual(metrics['active_players'],2)
self.assertEqual(metrics['robux'],250)
self.assertEqual(metrics['average_ccu'],2)
self.assertEqual(metrics['peak_ccu'],4)
self.assertEqual(metrics['known_ccu'],2)
first=reports_for_game(self.conn,self.game,today)['reports']
again=reports_for_game(self.conn,self.game,today)['reports']
self.assertEqual([r['periodDays'] for r in first],[1,7])
self.assertEqual(first[0]['generatedAt'],again[0]['generatedAt'])
self.assertEqual(first[0]['endDate'],self.yesterday.date().isoformat())
self.assertEqual(first[0]['metrics']['robux'],250)
self.assertEqual(len(first[0]['summaries']),6)
self.assertEqual(first[0]['summaryEvidence']['arppu'],125)
self.assertEqual(first[0]['summaryEvidence']['mau'],2)
self.assertIsNone(first[0]['summaryEvidence']['d7'])
self.assertEqual(first[0]['summaryEvidence']['promptSources'],{})
self.assertIn('Metric fixture',first[0]['executive']['paragraphs'][0]['text'])
self.assertTrue(first[0]['notes'])
self.assertEqual(change(1,0),'up from zero')
self.assertEqual(change(None,2),'comparison N/A')
def test_sale_price_is_independent_of_catalog_price(self):
self.conn.execute('UPDATE products SET current_price_robux=9999 WHERE game_id=%s',(self.game,))
self.assertEqual(monetization(self.conn,self.game,2)['metrics']['revenue'],250)
@@ -149,6 +246,34 @@ class SeededApi(unittest.TestCase):
cls.games=dispatch('/api/games',{})['games']
cls.game=cls.games[0]['id']
def test_hall_of_fame_and_seven_day_retention(self):
data=dispatch(f'/api/games/{self.game}/players',{})
self.assertEqual(data['pagination']['pageSize'],20)
for board in data['leaderboards']:
values=[p['value'] for p in board['players']]
self.assertEqual(len(values),5)
self.assertEqual(values,sorted(values,reverse=True))
week=dispatch(f'/api/games/{self.game}/retention',{'days':['30']})['weekRetention']
self.assertEqual(len(week['labels']),7)
self.assertGreater(week['players'],0)
self.assertTrue(all(v is None or 0<=v<=100 for v in week['series'][0]['values']))
def test_summaries_are_complete_per_game_and_period(self):
texts=[]
for game in self.games:
data=dispatch(f"/api/games/{game['id']}/reports",{})
for report in data['reports']:
self.assertEqual(report['gameId'],game['id'])
self.assertEqual(report['summaryVersion'],4)
self.assertEqual(len(report['summaries']),6)
self.assertEqual({r['key'] for r in report['summaries']},{'health','engagement','monetization','prompts','activities','investigations'})
self.assertGreater(len(report['executive']['paragraphs']),2)
self.assertEqual(len(report['executive']['paragraphs']),6)
self.assertIn('economyEvidence',report['executive'])
self.assertNotIn('Average CCU was',report['executive']['paragraphs'][0]['text'])
if report['periodDays']==7: texts.append(report['executive']['paragraphs'][0]['text'])
self.assertEqual(len(texts),len(set(texts)))
def test_daily_event_lines_match_summary(self):
data=dispatch(f'/api/games/{self.game}/events',{'days':['7']})
self.assertNotIn('events',data)
@@ -207,7 +332,8 @@ class ActivityStages(unittest.TestCase):
self.assertEqual(stages[1]['dropoffPercent'],25)
self.assertAlmostEqual(stages[2]['dropoffPercent'],200/3)
runs.append({'stage_progress':None})
self.assertIsNone(stage_funnel(runs,definition)[1]['dropoffPercent'])
self.assertEqual(stage_funnel(runs,definition)[1]['dropoffPercent'],25)
self.assertEqual(stage_funnel(runs,definition)[0]['runs'],4)
self.assertIsNone(stage_funnel([],definition)[0]['reachPercent'])
if __name__=='__main__': unittest.main()

View File

@@ -0,0 +1,65 @@
"""Exercise real Windows process cleanup without touching PostgreSQL."""
import ctypes
from ctypes import wintypes
import os
from pathlib import Path
import socket
import subprocess
import sys
import unittest
ROOT = Path(__file__).resolve().parents[2]
@unittest.skipUnless(os.name == 'nt', 'Windows launcher')
class LauncherLifecycle(unittest.TestCase):
def test_forced_owner_exit_stops_descendants(self):
kernel = ctypes.WinDLL('kernel32', use_last_error=True)
kernel.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
kernel.OpenProcess.restype = wintypes.HANDLE
kernel.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
kernel.WaitForSingleObject.restype = wintypes.DWORD
kernel.CloseHandle.argtypes = [wintypes.HANDLE]
code = '''from start_dashboard import TerminalJob
import subprocess, sys, time
job = TerminalJob()
child = subprocess.Popen([sys.executable, '-c', 'import time; time.sleep(60)'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
print(child.pid, flush=True)
time.sleep(60)
'''
owner = subprocess.Popen([sys.executable, '-u', '-c', code], cwd=ROOT,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
child_handle = None
try:
line = owner.stdout.readline().strip()
if not line:
self.fail(owner.communicate(timeout=5)[1])
child_handle = kernel.OpenProcess(0x100000, False, int(line)) # SYNCHRONIZE
self.assertTrue(child_handle)
owner.kill()
owner.wait(timeout=5)
self.assertEqual(kernel.WaitForSingleObject(child_handle, 5000), 0,
'A child survived the owner terminal process exiting.')
finally:
if owner.poll() is None:
owner.kill()
owner.communicate(timeout=5)
if child_handle:
kernel.CloseHandle(child_handle)
def test_busy_port_leaves_existing_service_alone(self):
with socket.socket() as listener:
listener.bind(('127.0.0.1', 0))
listener.listen()
env = dict(os.environ, MOCKUP_PORT=str(listener.getsockname()[1]))
result = subprocess.run([sys.executable, 'start_dashboard.py', '--no-browser'],
cwd=ROOT, env=env, capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 1)
self.assertTrue('already in use' in result.stderr or 'already running' in result.stderr,result.stderr)
with socket.create_connection(listener.getsockname(), timeout=1):
pass
if __name__ == '__main__':
unittest.main()