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

@@ -0,0 +1,30 @@
SET search_path TO analytics, public;
ALTER TABLE gamepass_ownership ADD COLUMN purchased_at timestamptz;
CREATE TABLE interaction_events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
game_id bigint NOT NULL REFERENCES games(id),
kind text NOT NULL,
report_key text NOT NULL,
player_id bigint,
session_id bigint,
product_id bigint,
receipt_id text,
started_at timestamptz NOT NULL,
ended_at timestamptz,
outcome text,
properties jsonb NOT NULL DEFAULT '{}',
UNIQUE(game_id,kind,report_key),
FOREIGN KEY(game_id,player_id) REFERENCES players(game_id,id),
FOREIGN KEY(game_id,player_id,session_id) REFERENCES sessions(game_id,player_id,id),
FOREIGN KEY(game_id,product_id) REFERENCES products(game_id,id),
FOREIGN KEY(game_id,receipt_id) REFERENCES product_sales(game_id,receipt_id),
CHECK(session_id IS NULL OR player_id IS NOT NULL),
CHECK(ended_at IS NULL OR ended_at>=started_at),
CHECK(kind<>'product_prompt' OR product_id IS NOT NULL),
CHECK(kind<>'product_prompt' OR outcome IS NULL OR outcome IN ('bought','closed')),
CHECK(outcome IS NULL OR ended_at IS NOT NULL)
);
CREATE INDEX interactions_game_time ON interaction_events(game_id,kind,started_at DESC);
CREATE INDEX interactions_player_time ON interaction_events(game_id,player_id,started_at DESC);
CREATE INDEX interactions_session ON interaction_events(game_id,player_id,session_id);
INSERT INTO schema_migrations(version) VALUES('004_player_records_and_interactions');

View File

@@ -0,0 +1,5 @@
SET search_path TO analytics, public;
ALTER TABLE interaction_events ADD COLUMN trigger_source text NOT NULL DEFAULT 'unknown';
ALTER TABLE interaction_events ADD CONSTRAINT interaction_trigger_source CHECK(trigger_source IN ('game','player','unknown'));
CREATE INDEX prompt_source_time ON interaction_events(game_id,kind,trigger_source,started_at DESC);
INSERT INTO schema_migrations(version) VALUES('005_prompt_sources');

View File

@@ -0,0 +1,10 @@
SET search_path TO analytics, public;
CREATE TABLE daily_health_reports (
game_id bigint NOT NULL REFERENCES games(id),
report_date date NOT NULL,
period_days integer NOT NULL CHECK(period_days IN (1,7)),
generated_at timestamptz NOT NULL DEFAULT now(),
payload jsonb NOT NULL,
PRIMARY KEY(game_id,report_date,period_days)
);
INSERT INTO schema_migrations(version) VALUES('006_daily_health_reports');

View File

@@ -9,7 +9,7 @@ ON CONFLICT(sample_key) DO NOTHING;
INSERT INTO activity_definitions(game_id,key,name,start_event,end_event,configuration)
SELECT g.id,d.key,d.name,d.start_event,d.end_event,d.config::jsonb FROM games g CROSS JOIN (VALUES
('shop','Shop visit','shop_opened','shop_closed','{"steps":["product_viewed","buy_prompt_shown","purchase_completed"],"fields":["product_id","purchase_amount","currency_balance"]}'),
('shop','Shop visit','shop_opened','shop_closed','{"steps":["product_viewed"],"fields":["currency_balance"]}'),
('minigame','Minigame run','minigame_started','minigame_ended','{"steps":["round_started","checkpoint_reached","round_ended"],"fields":["score","rounds"]}'),
('quest','Quest attempt','quest_started','quest_ended','{"steps":["objective_started","objective_completed"],"fields":["quest_id","reward"]}')
) d(key,name,start_event,end_event,config) WHERE g.sample_key IN ('willow-v1','orbit-v1','dungeon-v1');

View File

@@ -0,0 +1,35 @@
SET search_path TO analytics, public;
-- Only enrich the bundled synthetic histories; unknown real purchase dates stay NULL.
UPDATE gamepass_ownership o SET purchased_at=p.first_seen+(p.last_seen-p.first_seen)*((p.id*31+o.gamepass_id)%100/100.0)
FROM players p,games g WHERE p.id=o.player_id AND p.game_id=o.game_id AND g.id=o.game_id
AND g.sample_key IN ('willow-v1','orbit-v1','dungeon-v1') AND o.is_owned AND o.purchased_at IS NULL;
INSERT INTO interaction_events(game_id,kind,report_key,player_id,session_id,product_id,receipt_id,started_at,ended_at,outcome,properties)
SELECT s.game_id,'product_prompt','receipt-'||s.receipt_id,s.player_id,s.session_id,s.product_id,s.receipt_id,
greatest(se.started_at,s.purchased_at-(3+s.id%29)*interval '1 second'),s.purchased_at,'bought','{"synthetic":true}'
FROM product_sales s JOIN sessions se ON se.game_id=s.game_id AND se.id=s.session_id JOIN games g ON g.id=s.game_id
WHERE g.sample_key IN ('willow-v1','orbit-v1','dungeon-v1') ON CONFLICT DO NOTHING;
INSERT INTO interaction_events(game_id,kind,report_key,player_id,session_id,product_id,started_at,ended_at,outcome,properties)
SELECT s.game_id,'product_prompt','closed-session-'||s.id,s.player_id,s.id,p.id,
s.started_at+(s.ended_at-s.started_at)*.3,
CASE WHEN s.id%97=0 THEN NULL ELSE least(s.ended_at,s.started_at+(s.ended_at-s.started_at)*.3+(1+s.id%21)*interval '1 second') END,
CASE WHEN s.id%97=0 THEN NULL ELSE 'closed' END,'{"synthetic":true}'
FROM sessions s JOIN games g ON g.id=s.game_id JOIN products p ON p.game_id=s.game_id
AND p.roblox_product_id=8000000+s.game_id*100+1+s.id%4
WHERE g.sample_key IN ('willow-v1','orbit-v1','dungeon-v1') AND s.ended_at-s.started_at>interval '1 minute' AND s.id%3=0
ON CONFLICT DO NOTHING;
-- Product prompt telemetry is separate from the longer shop/activity journey.
-- Source labels below are synthetic examples, never inferred for real reports.
UPDATE interaction_events i SET trigger_source=CASE WHEN i.id%100<5 THEN 'unknown'
WHEN i.outcome='bought' AND i.id%100<85 THEN 'player'
WHEN i.outcome IS DISTINCT FROM 'bought' AND i.id%100<30 THEN 'player' ELSE 'game' END,
properties=i.properties||'{"sourceSeeded":true}'::jsonb
FROM games g WHERE i.game_id=g.id AND g.sample_key IN ('willow-v1','orbit-v1','dungeon-v1')
AND i.properties->>'synthetic'='true' AND NOT(i.properties ? 'sourceSeeded');
UPDATE activity_runs a SET prompt_count=0,outcome=CASE WHEN outcome='purchased' THEN 'closed' ELSE outcome END,
step_count=CASE WHEN stage_progress ? 'product_viewed' THEN 1 ELSE 0 END,
captured_values=captured_values-'product_id'-'purchase_amount',
stage_progress=(SELECT coalesce(jsonb_agg(e),'[]'::jsonb) FROM jsonb_array_elements(a.stage_progress) e WHERE e NOT IN ('"buy_prompt_shown"'::jsonb,'"purchase_completed"'::jsonb))
FROM activity_definitions d,games g WHERE d.id=a.definition_id AND d.game_id=a.game_id AND g.id=a.game_id
AND g.sample_key IN ('willow-v1','orbit-v1','dungeon-v1') AND d.key='shop';
UPDATE activity_definitions d SET configuration=jsonb_build_object('steps',jsonb_build_array('product_viewed'),'fields',jsonb_build_array('currency_balance'))
FROM games g WHERE d.game_id=g.id AND g.sample_key IN ('willow-v1','orbit-v1','dungeon-v1') AND d.key='shop';