"""Mockup editorial summaries using the saved report's game and UTC window.""" from collections import defaultdict from datetime import timedelta import statistics def extend_report(conn,report,start,end): from health_reports import number,change from sample_api import stage_funnel gid=report['gameId'];m=report['metrics'];prev=report['previousMetrics'] scope={'gid':gid,'start':start,'end':end} pct=lambda v: 'N/A' if v is None else number(v)+'%' minutes=lambda v: 'N/A' if v is None else number(v/60)+' min' hours=lambda v: 'N/A' if v is None else number(v/3600)+' h' money=lambda v,decimals=0: 'N/A' if v is None else 'R$ '+number(v,decimals) timing=lambda v: 'N/A' if v is None else number(v)+' sec' extra=conn.execute('''SELECT (SELECT count(DISTINCT player_id) FROM player_active_days WHERE game_id=%(gid)s AND active_day>=%(end)s::date-30 AND active_day<%(end)s::date)::int mau, avg(extract(epoch FROM s.ended_at-s.started_at)) FILTER(WHERE (s.started_at AT TIME ZONE 'UTC')::date=(p.first_seen AT TIME ZONE 'UTC')::date)::double precision new_session, avg(extract(epoch FROM s.ended_at-s.started_at)) FILTER(WHERE (s.started_at AT TIME ZONE 'UTC')::date>(p.first_seen AT TIME ZONE 'UTC')::date)::double precision returning_session FROM sessions s JOIN players p ON p.game_id=s.game_id AND p.id=s.player_id WHERE s.game_id=%(gid)s AND s.started_at>=%(start)s::date AND s.started_at<%(end)s::date''',scope).fetchone() retained={} for offset in (7,30): r=conn.execute('''SELECT count(*)::int cohort,count(*) FILTER(WHERE EXISTS(SELECT 1 FROM sessions s WHERE s.game_id=p.game_id AND s.player_id=p.id AND s.started_at>=(p.first_seen AT TIME ZONE 'UTC')::date+%(offset)s AND s.started_at<(p.first_seen AT TIME ZONE 'UTC')::date+%(offset)s+1))::int returned FROM players p WHERE p.game_id=%(gid)s AND p.first_seen>=%(start)s::date-%(offset)s AND p.first_seen<%(end)s::date-%(offset)s''',scope|{'offset':offset}).fetchone() retained[offset]=100*r['returned']/r['cohort'] if r['cohort'] else None products=conn.execute('''SELECT p.id,p.name,sum(s.quantity)::int units,sum(s.unit_price_robux*s.quantity)::double precision robux FROM product_sales s JOIN products p ON p.game_id=s.game_id AND p.id=s.product_id WHERE s.game_id=%(gid)s AND s.purchased_at>=%(start)s::date AND s.purchased_at<%(end)s::date GROUP BY p.id ORDER BY robux DESC,p.id''',scope).fetchall() payers=conn.execute('''SELECT player_id,sum(unit_price_robux*quantity)::double precision robux FROM product_sales WHERE game_id=%(gid)s AND purchased_at>=%(start)s::date AND purchased_at<%(end)s::date AND unit_price_robux>0 AND player_id IS NOT NULL GROUP BY player_id ORDER BY robux DESC,player_id''',scope).fetchall() largest=100*payers[0]['robux']/m['robux'] if payers and m['robux'] and not m['unattributed'] else None arppu=m['robux']/m['payers'] if m['payers'] and not m['unattributed'] else None user_days=m['average_dau']*report['periodDays'] arpdau=m['robux']/user_days if user_days else None prompts=conn.execute('''SELECT trigger_source source,count(*)::int prompts,count(*) FILTER(WHERE outcome='bought')::int bought, count(*) FILTER(WHERE outcome='closed')::int closed,count(*) FILTER(WHERE outcome IS NULL)::int unresolved, avg(extract(epoch FROM ended_at-started_at)) FILTER(WHERE outcome='bought')::double precision buy_time, avg(extract(epoch FROM ended_at-started_at)) FILTER(WHERE outcome='closed')::double precision close_time FROM interaction_events WHERE game_id=%(gid)s AND kind='product_prompt' AND outcome IN ('bought','closed') AND ended_at IS NOT NULL AND trigger_source IN ('game','player') AND started_at>=%(start)s::date AND started_at<%(end)s::date GROUP BY trigger_source''',scope).fetchall() prompt_by_source={r['source']:dict(r,conversion=100*r['bought']/r['prompts'] if r['prompts'] else None) for r in prompts} game_prompt=prompt_by_source.get('game',{});player_prompt=prompt_by_source.get('player',{}) definitions=conn.execute('SELECT * FROM activity_definitions WHERE game_id=%s ORDER BY id',(gid,)).fetchall() runs=conn.execute('''SELECT definition_id,stage_progress,extract(epoch FROM ended_at-started_at)::double precision duration FROM activity_runs WHERE game_id=%(gid)s AND ended_at IS NOT NULL AND outcome IS NOT NULL AND started_at>=%(start)s::date AND started_at<%(end)s::date''',scope).fetchall() grouped=defaultdict(list) for row in runs: grouped[row['definition_id']].append(row) activity_lines=[];drops=[] for definition in definitions: members=grouped[definition['id']] if not members: continue times=[r['duration'] for r in members if r['duration'] is not None] activity_lines.append(f"{definition['name']}: {number(len(members),0)} runs, {minutes(statistics.mean(times) if times else None)} average known duration.") funnel=stage_funnel(members,definition) for i,stage in enumerate(funnel[1:],1): if stage['dropoffPercent'] is not None and funnel[i-1]['runs']: drops.append({'activity':definition['name'],'from':funnel[i-1]['event'],'to':stage['event'], 'dropoff':stage['dropoffPercent'],'previousRuns':funnel[i-1]['runs'],'reach':stage['reachPercent']}) biggest=max(drops,key=lambda r:r['dropoff']) if drops else None largest_drop=(f"The largest observed stage loss was in {biggest['activity']}: {pct(biggest['dropoff'])} from {biggest['from']} to {biggest['to']}, among {number(biggest['previousRuns'],0)} runs at the preceding stage. {pct(biggest['reach'])} of all starts reached that stage." if biggest else 'Stage drop-off is N/A because no complete ordered funnel observations are available.') game=conn.execute('SELECT sample_key FROM games WHERE id=%s',(gid,)).fetchone() angle={ 'willow-v1':'For this social exploration game, inspect whether collection and exploration encourage another visit after the first session.', 'orbit-v1':'For this racing game, inspect how quickly a new player reaches a first race and whether the post-race flow encourages another round.', 'dungeon-v1':'For this progression game, inspect whether early crafting and quest rewards give new players a clear reason to return.' }.get(game['sample_key'],'Inspect the onboarding and progression steps behind these observed changes.') audience_grew=m['active_players']>prev['active_players'] revenue_grew=m['robux']>prev['robux'] assessment=('Audience and spending both grew.' if audience_grew and revenue_grew else 'Spending grew while the active audience contracted or stayed flat.' if revenue_grew else 'The audience grew while spending declined or stayed flat.' if audience_grew else 'Active reach and spending were flat or lower than the preceding period.') top_product=(f"{products[0]['name']} led recorded product spending with {money(products[0]['robux'])} from {number(products[0]['units'],0)} units." if products else 'No product sales were reported in this window.') prompt_lines=[] for key,title in [('game','Game-induced'),('player','Player-induced')]: p=prompt_by_source.get(key) if not p: prompt_lines.append(f'{title}: no reported prompts.');continue prompt_lines.append(f"{title}: {number(p['prompts'],0)} prompts, {pct(p['conversion'])} bought; average time to buy {timing(p['buy_time'])}, to close {timing(p['close_time'])}.") engagement_text=f"{report['gameName']} recorded {number(m['sessions'],0)} session starts and {hours(m['total_playtime'])} of known playtime. Average completed sessions lasted {minutes(m['average_session'])} ({change(m['average_session'],prev['average_session'])} versus the prior period). New-user sessions averaged {minutes(extra['new_session'])}; returning-user sessions averaged {minutes(extra['returning_session'])}." retention_text=f"Five-minute first-session retention was {pct(m['first_five_rate'])}. Mature exact-day returns were D1 {pct(m['retention'])}, D7 {pct(retained[7])}, and D30 {pct(retained[30])}; these use cohorts whose observation day fell inside this report window." money_text=f"Reported spending reached {money(m['robux'])} ({change(m['robux'],prev['robux'])}), with {number(m['payers'],0) if not m['unattributed'] else 'N/A'} paying players and {pct(m['payer_conversion'])} period payer conversion. ARPPU was {money(arppu,2)}; Robux per active user-day was {money(arpdau,2)}." concentration=f"The largest recorded payer contributed {pct(largest)} of period spending. Check whether revenue changes persist without that player before treating them as a broad spending trend." if largest is not None else 'Payer concentration is N/A when no attributed positive-price sales are available.' investigations=[ {'title':'First-session experience','text':f"Five-minute first-session retention: {pct(m['first_five_rate'])}. {angle}"}, {'title':'Activity transition','text':largest_drop+' Review the preceding step before changing the entire activity.' if biggest else largest_drop}, {'title':'Offer placement','text':f"Player-induced prompts converted at {pct(player_prompt.get('conversion'))}; game-induced prompts at {pct(game_prompt.get('conversion'))}. Compare the same products and similar player groups before testing offer timing or frequency."} ] sections=[ {'key':'health','title':'Game health','target':'overview','paragraphs':report['paragraphs']}, {'key':'engagement','title':'Engagement & retention','target':'sessions','paragraphs':[{'title':'Session quality','text':engagement_text},{'title':'Returning players','text':retention_text},{'title':'What to inspect','text':angle}]}, {'key':'monetization','title':'Monetization','target':'monetization','paragraphs':[{'title':'Spending & conversion','text':money_text},{'title':'Products','text':top_product},{'title':'Payer concentration','text':concentration}]}, {'key':'prompts','title':'Purchase prompts','target':'prompts','paragraphs':[{'title':'Source breakdown','text':' '.join(prompt_lines)},{'title':'Interpretation','text':'Source comparisons describe different reported audiences, not a proven effect of prompting. Use matching products and player segments to decide which placement to test.'}]}, {'key':'activities','title':'Activities & funnels','target':'activities','paragraphs':[{'title':'Activity use','text':' '.join(activity_lines) or 'No activity runs were reported in this window.'},{'title':'Stage drop-off','text':largest_drop}]}, {'key':'investigations','title':'Suggested investigations','target':None,'paragraphs':investigations} ] evidence={'mau':extra['mau'],'d7':retained[7],'d30':retained[30],'arppu':arppu,'arpdau':arpdau, 'newSession':extra['new_session'],'returningSession':extra['returning_session'], 'largestPayerShare':largest,'topProducts':products,'promptSources':prompt_by_source,'largestDrop':biggest} from overview_analysis import deep_analysis executive=deep_analysis(conn,report,start,end,evidence) return {'summaryVersion':4,'executive':executive,'summaries':sections,'summaryEvidence':evidence}