Expand dashboard analytics and document production implementation
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user