nordabiz/database/migrations/014_user_analytics.sql
Maciej Pienczyn a148d464af feat: Add user analytics panel (/admin/analytics)
- Track user sessions, page views, clicks
- Measure engagement score and session duration
- Device breakdown (desktop/mobile/tablet)
- User rankings by activity
- Popular pages statistics
- Recent sessions feed
- SQL migration for analytics tables
- JavaScript tracker for frontend events

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 19:40:37 +01:00

251 lines
8.5 KiB
PL/PgSQL

-- Migration: 014_user_analytics.sql
-- Description: User activity tracking and analytics
-- Created: 2026-01-13
-- Author: Claude
-- ============================================================
-- TABELE ANALITYKI UŻYTKOWNIKÓW
-- ============================================================
-- Sesje użytkowników
CREATE TABLE IF NOT EXISTS user_sessions (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
session_id VARCHAR(100) UNIQUE NOT NULL,
-- Czas sesji
started_at TIMESTAMP NOT NULL DEFAULT NOW(),
ended_at TIMESTAMP,
last_activity_at TIMESTAMP NOT NULL DEFAULT NOW(),
duration_seconds INTEGER,
-- Urządzenie
ip_address VARCHAR(45),
user_agent TEXT,
device_type VARCHAR(20), -- desktop, mobile, tablet
browser VARCHAR(50),
browser_version VARCHAR(20),
os VARCHAR(50),
os_version VARCHAR(20),
-- Lokalizacja (z IP)
country VARCHAR(100),
city VARCHAR(100),
region VARCHAR(100),
-- Metryki sesji
page_views_count INTEGER DEFAULT 0,
clicks_count INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_user_sessions_user ON user_sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_user_sessions_started ON user_sessions(started_at);
CREATE INDEX IF NOT EXISTS idx_user_sessions_session_id ON user_sessions(session_id);
-- Wyświetlenia stron
CREATE TABLE IF NOT EXISTS page_views (
id SERIAL PRIMARY KEY,
session_id INTEGER REFERENCES user_sessions(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
-- Strona
url VARCHAR(2000) NOT NULL,
path VARCHAR(500) NOT NULL,
page_title VARCHAR(500),
referrer VARCHAR(2000),
-- Czas
viewed_at TIMESTAMP NOT NULL DEFAULT NOW(),
time_on_page_seconds INTEGER,
-- Kontekst
company_id INTEGER REFERENCES companies(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_page_views_session ON page_views(session_id);
CREATE INDEX IF NOT EXISTS idx_page_views_user ON page_views(user_id);
CREATE INDEX IF NOT EXISTS idx_page_views_path ON page_views(path);
CREATE INDEX IF NOT EXISTS idx_page_views_viewed ON page_views(viewed_at);
CREATE INDEX IF NOT EXISTS idx_page_views_company ON page_views(company_id);
-- Kliknięcia użytkowników
CREATE TABLE IF NOT EXISTS user_clicks (
id SERIAL PRIMARY KEY,
session_id INTEGER REFERENCES user_sessions(id) ON DELETE CASCADE,
page_view_id INTEGER REFERENCES page_views(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
-- Element kliknięty
element_type VARCHAR(50), -- button, link, card, nav, form
element_id VARCHAR(100),
element_text VARCHAR(255),
element_class VARCHAR(500),
target_url VARCHAR(2000),
-- Pozycja
x_position INTEGER,
y_position INTEGER,
clicked_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_user_clicks_session ON user_clicks(session_id);
CREATE INDEX IF NOT EXISTS idx_user_clicks_element ON user_clicks(element_type);
CREATE INDEX IF NOT EXISTS idx_user_clicks_clicked ON user_clicks(clicked_at);
-- Dzienne statystyki (agregowane)
CREATE TABLE IF NOT EXISTS analytics_daily (
id SERIAL PRIMARY KEY,
date DATE UNIQUE NOT NULL,
-- Sesje
total_sessions INTEGER DEFAULT 0,
unique_users INTEGER DEFAULT 0,
new_users INTEGER DEFAULT 0,
returning_users INTEGER DEFAULT 0,
anonymous_sessions INTEGER DEFAULT 0,
-- Aktywność
total_page_views INTEGER DEFAULT 0,
total_clicks INTEGER DEFAULT 0,
avg_session_duration_seconds INTEGER,
avg_pages_per_session NUMERIC(5,2),
-- Urządzenia
desktop_sessions INTEGER DEFAULT 0,
mobile_sessions INTEGER DEFAULT 0,
tablet_sessions INTEGER DEFAULT 0,
-- Engagement
bounce_rate NUMERIC(5,2), -- % sesji z 1 page view
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_analytics_daily_date ON analytics_daily(date);
-- Popularne strony (dzienne)
CREATE TABLE IF NOT EXISTS popular_pages_daily (
id SERIAL PRIMARY KEY,
date DATE NOT NULL,
path VARCHAR(500) NOT NULL,
page_title VARCHAR(500),
views_count INTEGER DEFAULT 0,
unique_visitors INTEGER DEFAULT 0,
avg_time_seconds INTEGER,
UNIQUE(date, path)
);
CREATE INDEX IF NOT EXISTS idx_popular_pages_date ON popular_pages_daily(date);
-- ============================================================
-- GRANT PERMISSIONS
-- ============================================================
GRANT ALL ON TABLE user_sessions TO nordabiz_app;
GRANT ALL ON TABLE page_views TO nordabiz_app;
GRANT ALL ON TABLE user_clicks TO nordabiz_app;
GRANT ALL ON TABLE analytics_daily TO nordabiz_app;
GRANT ALL ON TABLE popular_pages_daily TO nordabiz_app;
GRANT USAGE, SELECT ON SEQUENCE user_sessions_id_seq TO nordabiz_app;
GRANT USAGE, SELECT ON SEQUENCE page_views_id_seq TO nordabiz_app;
GRANT USAGE, SELECT ON SEQUENCE user_clicks_id_seq TO nordabiz_app;
GRANT USAGE, SELECT ON SEQUENCE analytics_daily_id_seq TO nordabiz_app;
GRANT USAGE, SELECT ON SEQUENCE popular_pages_daily_id_seq TO nordabiz_app;
-- ============================================================
-- TRIGGER: Update analytics_daily on new session/page_view
-- ============================================================
-- Function to update daily analytics
CREATE OR REPLACE FUNCTION update_analytics_daily()
RETURNS TRIGGER AS $$
DECLARE
target_date DATE;
BEGIN
IF TG_TABLE_NAME = 'user_sessions' THEN
target_date := DATE(NEW.started_at);
ELSIF TG_TABLE_NAME = 'page_views' THEN
target_date := DATE(NEW.viewed_at);
ELSE
RETURN NEW;
END IF;
-- Insert or update daily stats
INSERT INTO analytics_daily (date, total_sessions, total_page_views, updated_at)
VALUES (target_date, 0, 0, NOW())
ON CONFLICT (date) DO NOTHING;
-- Update counts based on trigger source
IF TG_TABLE_NAME = 'user_sessions' THEN
UPDATE analytics_daily SET
total_sessions = total_sessions + 1,
unique_users = (
SELECT COUNT(DISTINCT user_id)
FROM user_sessions
WHERE DATE(started_at) = target_date AND user_id IS NOT NULL
),
anonymous_sessions = (
SELECT COUNT(*)
FROM user_sessions
WHERE DATE(started_at) = target_date AND user_id IS NULL
),
desktop_sessions = (
SELECT COUNT(*) FROM user_sessions
WHERE DATE(started_at) = target_date AND device_type = 'desktop'
),
mobile_sessions = (
SELECT COUNT(*) FROM user_sessions
WHERE DATE(started_at) = target_date AND device_type = 'mobile'
),
tablet_sessions = (
SELECT COUNT(*) FROM user_sessions
WHERE DATE(started_at) = target_date AND device_type = 'tablet'
),
updated_at = NOW()
WHERE date = target_date;
ELSIF TG_TABLE_NAME = 'page_views' THEN
UPDATE analytics_daily SET
total_page_views = total_page_views + 1,
updated_at = NOW()
WHERE date = target_date;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create triggers
DROP TRIGGER IF EXISTS trg_update_analytics_on_session ON user_sessions;
CREATE TRIGGER trg_update_analytics_on_session
AFTER INSERT ON user_sessions
FOR EACH ROW
EXECUTE FUNCTION update_analytics_daily();
DROP TRIGGER IF EXISTS trg_update_analytics_on_pageview ON page_views;
CREATE TRIGGER trg_update_analytics_on_pageview
AFTER INSERT ON page_views
FOR EACH ROW
EXECUTE FUNCTION update_analytics_daily();
-- ============================================================
-- COMMENTS
-- ============================================================
COMMENT ON TABLE user_sessions IS 'Sesje użytkowników portalu NordaBiznes';
COMMENT ON TABLE page_views IS 'Wyświetlenia stron przez użytkowników';
COMMENT ON TABLE user_clicks IS 'Kliknięcia elementów na stronach';
COMMENT ON TABLE analytics_daily IS 'Dzienne statystyki agregowane';
COMMENT ON TABLE popular_pages_daily IS 'Popularne strony - dzienne agregaty';
COMMENT ON COLUMN user_sessions.device_type IS 'Typ urządzenia: desktop, mobile, tablet';
COMMENT ON COLUMN user_sessions.duration_seconds IS 'Czas trwania sesji w sekundach';
COMMENT ON COLUMN page_views.time_on_page_seconds IS 'Czas spędzony na stronie w sekundach';
COMMENT ON COLUMN analytics_daily.bounce_rate IS 'Procent sesji z tylko 1 wyświetleniem strony';