Add PostgreSQL analytics dashboard prototype and realistic sample data

This commit is contained in:
maxwes08
2026-11-03 09:13:00 +01:00
commit 70b94f8672
38 changed files with 2873 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
SET search_path TO analytics, public;
CREATE TABLE products (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
game_id bigint NOT NULL REFERENCES games(id),
roblox_product_id bigint NOT NULL,
name text,
current_price_robux bigint CHECK(current_price_robux>=0),
UNIQUE(game_id,id), UNIQUE(game_id,roblox_product_id)
);
CREATE TABLE product_sales (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
game_id bigint NOT NULL REFERENCES games(id),
product_id bigint NOT NULL,
player_id bigint,
session_id bigint,
receipt_id text NOT NULL,
purchased_at timestamptz NOT NULL,
unit_price_robux bigint NOT NULL CHECK(unit_price_robux>=0),
quantity integer NOT NULL DEFAULT 1 CHECK(quantity>0),
properties jsonb NOT NULL DEFAULT '{}',
UNIQUE(game_id,receipt_id),
FOREIGN KEY(game_id,product_id) REFERENCES products(game_id,id),
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),
CHECK(session_id IS NULL OR player_id IS NOT NULL)
);
CREATE INDEX sales_game_time ON product_sales(game_id,purchased_at DESC);
CREATE INDEX sales_player_time ON product_sales(game_id,player_id,purchased_at DESC);
CREATE INDEX sales_product_time ON product_sales(game_id,product_id,purchased_at DESC);
CREATE OR REPLACE VIEW player_statistics AS
WITH session_totals AS (
SELECT game_id,player_id,count(*)::int session_count,
sum(extract(epoch FROM ended_at-started_at))::double precision playtime,
avg(extract(epoch FROM ended_at-started_at))::double precision average_session,
percentile_cont(.5) WITHIN GROUP(ORDER BY extract(epoch FROM ended_at-started_at))::double precision median_session,
count(DISTINCT (started_at AT TIME ZONE 'UTC')::date) FILTER(WHERE started_at>=now()-interval '30 days')::int active_days
FROM sessions GROUP BY game_id,player_id
), spend AS (
SELECT game_id,player_id,sum(unit_price_robux*quantity)::double precision robux,sum(quantity)::int products_bought
FROM product_sales WHERE unit_price_robux>0 AND player_id IS NOT NULL GROUP BY game_id,player_id
)
SELECT p.*,latest.data latest_data,latest.captured_at snapshot_at,
coalesce(s.session_count,0) session_count,s.playtime,s.average_session,s.median_session,coalesce(s.active_days,0) active_days,
coalesce(spend.robux,0) robux_recorded,coalesce(spend.products_bought,0) products_bought
FROM players p LEFT JOIN session_totals s ON s.game_id=p.game_id AND s.player_id=p.id
LEFT JOIN spend ON spend.game_id=p.game_id AND spend.player_id=p.id
LEFT JOIN LATERAL(SELECT data,captured_at FROM player_snapshots ps WHERE ps.game_id=p.game_id AND ps.player_id=p.id ORDER BY captured_at DESC,id DESC LIMIT 1) latest ON true;
CREATE VIEW session_day_playtime AS
SELECT s.game_id,s.player_id,s.id session_id,d.day::date active_day,
CASE WHEN s.ended_at IS NOT NULL THEN greatest(0,extract(epoch FROM least(s.ended_at,d.day+interval '1 day')-greatest(s.started_at,d.day)))::double precision END playtime
FROM sessions s CROSS JOIN LATERAL generate_series(date_trunc('day',s.started_at),date_trunc('day',coalesce(s.ended_at,s.started_at)),interval '1 day') d(day);
CREATE VIEW player_active_days AS
SELECT game_id,player_id,active_day FROM session_day_playtime
UNION SELECT game_id,player_id,(occurred_at AT TIME ZONE 'UTC')::date FROM events WHERE player_id IS NOT NULL
UNION SELECT game_id,player_id,(purchased_at AT TIME ZONE 'UTC')::date FROM product_sales WHERE player_id IS NOT NULL;
INSERT INTO schema_migrations(version) VALUES('002_monetization');