Expand dashboard analytics and document production implementation
This commit is contained in:
30
mockups/db/004_player_records_and_interactions.sql
Normal file
30
mockups/db/004_player_records_and_interactions.sql
Normal 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');
|
||||
5
mockups/db/005_prompt_sources.sql
Normal file
5
mockups/db/005_prompt_sources.sql
Normal 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');
|
||||
10
mockups/db/006_daily_health_reports.sql
Normal file
10
mockups/db/006_daily_health_reports.sql
Normal 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');
|
||||
@@ -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');
|
||||
|
||||
35
mockups/db/seed_interactions.sql
Normal file
35
mockups/db/seed_interactions.sql
Normal 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';
|
||||
Reference in New Issue
Block a user