nordabiz/database/migrations/033_analytics_expansion.sql
Maciej Pienczyn 0055857df4 feat: Rozbudowa systemu analityki użytkowników
Nowe funkcjonalności:
- GeoIP enrichment (kraj, miasto, region)
- UTM parameters tracking (source, medium, campaign, term, content)
- Bounce rate calculation
- Search queries logging
- Conversion tracking (register, login, contact_click, rsvp)
- Scroll depth tracking (25%, 50%, 75%, 100%)
- JS error tracking (window.onerror)
- Performance metrics (Web Vitals)
- CSV export (sessions, pageviews, searches, conversions)

Nowe tabele SQL:
- search_queries
- conversion_events
- js_errors
- popular_searches_daily
- hourly_activity

Dashboard rozszerzony o nowe sekcje i metryki.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 15:52:18 +01:00

265 lines
12 KiB
PL/PgSQL

-- ============================================================
-- Migration: 033_analytics_expansion.sql
-- Description: Rozbudowa systemu analityki użytkowników
-- Author: Claude
-- Date: 2026-01-30
-- ============================================================
BEGIN;
-- ============================================================
-- 1. UTM PARAMETERS - Dodanie do user_sessions
-- ============================================================
ALTER TABLE user_sessions ADD COLUMN IF NOT EXISTS utm_source VARCHAR(255);
ALTER TABLE user_sessions ADD COLUMN IF NOT EXISTS utm_medium VARCHAR(255);
ALTER TABLE user_sessions ADD COLUMN IF NOT EXISTS utm_campaign VARCHAR(255);
ALTER TABLE user_sessions ADD COLUMN IF NOT EXISTS utm_term VARCHAR(255);
ALTER TABLE user_sessions ADD COLUMN IF NOT EXISTS utm_content VARCHAR(255);
-- Indeks dla raportowania kampanii
CREATE INDEX IF NOT EXISTS idx_user_sessions_utm_source ON user_sessions(utm_source) WHERE utm_source IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_user_sessions_utm_campaign ON user_sessions(utm_campaign) WHERE utm_campaign IS NOT NULL;
COMMENT ON COLUMN user_sessions.utm_source IS 'Źródło ruchu: google, facebook, newsletter';
COMMENT ON COLUMN user_sessions.utm_medium IS 'Medium: cpc, email, social, organic';
COMMENT ON COLUMN user_sessions.utm_campaign IS 'Nazwa kampanii marketingowej';
COMMENT ON COLUMN user_sessions.utm_term IS 'Słowo kluczowe (dla reklam PPC)';
COMMENT ON COLUMN user_sessions.utm_content IS 'Wariant reklamy/linku';
-- ============================================================
-- 2. SCROLL DEPTH - Dodanie do page_views
-- ============================================================
ALTER TABLE page_views ADD COLUMN IF NOT EXISTS scroll_depth_percent INTEGER;
COMMENT ON COLUMN page_views.scroll_depth_percent IS 'Maksymalny % przewinięcia strony (0-100)';
-- ============================================================
-- 3. SEARCH QUERIES - Nowa tabela
-- ============================================================
CREATE TABLE IF NOT EXISTS search_queries (
id SERIAL PRIMARY KEY,
session_id INTEGER REFERENCES user_sessions(id) ON DELETE SET NULL,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
-- Zapytanie
query VARCHAR(500) NOT NULL,
query_normalized VARCHAR(500), -- lowercase, bez znaków specjalnych
-- Wyniki
results_count INTEGER DEFAULT 0,
has_results BOOLEAN DEFAULT TRUE,
-- Interakcja z wynikami
clicked_result_position INTEGER, -- Która pozycja była kliknięta (1-based)
clicked_company_id INTEGER REFERENCES companies(id) ON DELETE SET NULL,
-- Kontekst
search_type VARCHAR(50) DEFAULT 'main', -- main, chat, autocomplete
filters_used JSONB, -- {"category": "IT", "city": "Wejherowo"}
-- Timing
searched_at TIMESTAMP NOT NULL DEFAULT NOW(),
time_to_click_ms INTEGER, -- Czas od wyświetlenia do kliknięcia
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_search_queries_query ON search_queries(query_normalized);
CREATE INDEX IF NOT EXISTS idx_search_queries_searched_at ON search_queries(searched_at);
CREATE INDEX IF NOT EXISTS idx_search_queries_session ON search_queries(session_id);
CREATE INDEX IF NOT EXISTS idx_search_queries_has_results ON search_queries(has_results) WHERE has_results = FALSE;
COMMENT ON TABLE search_queries IS 'Historia wyszukiwań użytkowników w portalu';
-- ============================================================
-- 4. CONVERSION EVENTS - Nowa tabela
-- ============================================================
CREATE TABLE IF NOT EXISTS conversion_events (
id SERIAL PRIMARY KEY,
session_id INTEGER REFERENCES user_sessions(id) ON DELETE SET NULL,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
-- Typ konwersji
event_type VARCHAR(50) NOT NULL, -- register, login, contact_click, rsvp, message, classified
event_category VARCHAR(50), -- engagement, acquisition, activation
-- Kontekst
company_id INTEGER REFERENCES companies(id) ON DELETE SET NULL, -- Dla contact_click
target_type VARCHAR(50), -- email, phone, website, rsvp_event, etc.
target_value VARCHAR(500), -- np. adres email, id eventu
-- Źródło konwersji
source_page VARCHAR(500), -- URL strony na której nastąpiła konwersja
referrer VARCHAR(500),
-- Dodatkowe dane
event_metadata JSONB, -- Elastyczne dane kontekstowe
-- Timing
converted_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_conversion_events_type ON conversion_events(event_type);
CREATE INDEX IF NOT EXISTS idx_conversion_events_converted_at ON conversion_events(converted_at);
CREATE INDEX IF NOT EXISTS idx_conversion_events_session ON conversion_events(session_id);
CREATE INDEX IF NOT EXISTS idx_conversion_events_user ON conversion_events(user_id);
CREATE INDEX IF NOT EXISTS idx_conversion_events_company ON conversion_events(company_id) WHERE company_id IS NOT NULL;
COMMENT ON TABLE conversion_events IS 'Kluczowe konwersje: rejestracje, kontakty, RSVP';
-- ============================================================
-- 5. JS ERRORS - Nowa tabela
-- ============================================================
CREATE TABLE IF NOT EXISTS js_errors (
id SERIAL PRIMARY KEY,
session_id INTEGER REFERENCES user_sessions(id) ON DELETE SET NULL,
-- Błąd
message TEXT NOT NULL,
source VARCHAR(500), -- URL pliku JS
lineno INTEGER,
colno INTEGER,
stack TEXT, -- Stack trace
-- Kontekst
url VARCHAR(2000), -- URL strony gdzie wystąpił błąd
user_agent VARCHAR(500),
-- Agregacja
error_hash VARCHAR(64), -- SHA256 z message+source+lineno dla grupowania
occurred_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_js_errors_hash ON js_errors(error_hash);
CREATE INDEX IF NOT EXISTS idx_js_errors_occurred_at ON js_errors(occurred_at);
CREATE INDEX IF NOT EXISTS idx_js_errors_source ON js_errors(source);
COMMENT ON TABLE js_errors IS 'Błędy JavaScript zgłaszane z przeglądarek użytkowników';
-- ============================================================
-- 6. PERFORMANCE METRICS - Dodanie do page_views
-- ============================================================
ALTER TABLE page_views ADD COLUMN IF NOT EXISTS dom_content_loaded_ms INTEGER;
ALTER TABLE page_views ADD COLUMN IF NOT EXISTS load_time_ms INTEGER;
ALTER TABLE page_views ADD COLUMN IF NOT EXISTS first_paint_ms INTEGER;
ALTER TABLE page_views ADD COLUMN IF NOT EXISTS first_contentful_paint_ms INTEGER;
COMMENT ON COLUMN page_views.dom_content_loaded_ms IS 'Czas do DOMContentLoaded (ms)';
COMMENT ON COLUMN page_views.load_time_ms IS 'Pełny czas ładowania strony (ms)';
COMMENT ON COLUMN page_views.first_paint_ms IS 'Czas do First Paint (ms)';
COMMENT ON COLUMN page_views.first_contentful_paint_ms IS 'Czas do First Contentful Paint (ms)';
-- ============================================================
-- 7. AGREGATY DZIENNE - Rozszerzenie analytics_daily
-- ============================================================
-- Bounce rate (już istnieje, ale upewniamy się)
ALTER TABLE analytics_daily ADD COLUMN IF NOT EXISTS bounce_rate NUMERIC(5,2);
-- Nowe metryki
ALTER TABLE analytics_daily ADD COLUMN IF NOT EXISTS conversions_count INTEGER DEFAULT 0;
ALTER TABLE analytics_daily ADD COLUMN IF NOT EXISTS searches_count INTEGER DEFAULT 0;
ALTER TABLE analytics_daily ADD COLUMN IF NOT EXISTS searches_no_results INTEGER DEFAULT 0;
ALTER TABLE analytics_daily ADD COLUMN IF NOT EXISTS avg_scroll_depth NUMERIC(5,2);
ALTER TABLE analytics_daily ADD COLUMN IF NOT EXISTS js_errors_count INTEGER DEFAULT 0;
-- UTM breakdown (JSONB dla elastyczności)
ALTER TABLE analytics_daily ADD COLUMN IF NOT EXISTS utm_breakdown JSONB;
-- Konwersje wg typu
ALTER TABLE analytics_daily ADD COLUMN IF NOT EXISTS conversions_breakdown JSONB;
COMMENT ON COLUMN analytics_daily.bounce_rate IS 'Procent sesji z 1 pageview lub <10s';
COMMENT ON COLUMN analytics_daily.conversions_count IS 'Łączna liczba konwersji';
COMMENT ON COLUMN analytics_daily.searches_count IS 'Liczba wyszukiwań';
COMMENT ON COLUMN analytics_daily.searches_no_results IS 'Wyszukiwania bez wyników';
COMMENT ON COLUMN analytics_daily.avg_scroll_depth IS 'Średnia głębokość scrollowania (%)';
COMMENT ON COLUMN analytics_daily.utm_breakdown IS 'Rozkład sesji wg UTM source';
COMMENT ON COLUMN analytics_daily.conversions_breakdown IS 'Rozkład konwersji wg typu';
-- ============================================================
-- 8. POPULAR SEARCHES - Agregat dzienny
-- ============================================================
CREATE TABLE IF NOT EXISTS popular_searches_daily (
id SERIAL PRIMARY KEY,
date DATE NOT NULL,
query_normalized VARCHAR(500) NOT NULL,
search_count INTEGER DEFAULT 0,
unique_users INTEGER DEFAULT 0,
click_count INTEGER DEFAULT 0, -- Ile razy kliknięto wynik
avg_results_count NUMERIC(10,2),
UNIQUE(date, query_normalized)
);
CREATE INDEX IF NOT EXISTS idx_popular_searches_date ON popular_searches_daily(date);
COMMENT ON TABLE popular_searches_daily IS 'Popularne wyszukiwania - dzienne agregaty';
-- ============================================================
-- 9. HOURLY ACTIVITY - Dla wzorców czasowych
-- ============================================================
CREATE TABLE IF NOT EXISTS hourly_activity (
id SERIAL PRIMARY KEY,
date DATE NOT NULL,
hour INTEGER NOT NULL CHECK (hour >= 0 AND hour <= 23),
sessions_count INTEGER DEFAULT 0,
page_views_count INTEGER DEFAULT 0,
unique_users INTEGER DEFAULT 0,
UNIQUE(date, hour)
);
CREATE INDEX IF NOT EXISTS idx_hourly_activity_date ON hourly_activity(date);
COMMENT ON TABLE hourly_activity IS 'Aktywność wg godziny - dla analizy wzorców czasowych';
-- ============================================================
-- 10. UPRAWNIENIA
-- ============================================================
-- Nadanie uprawnień dla roli nordabiz_app
GRANT ALL ON TABLE search_queries TO nordabiz_app;
GRANT ALL ON TABLE conversion_events TO nordabiz_app;
GRANT ALL ON TABLE js_errors TO nordabiz_app;
GRANT ALL ON TABLE popular_searches_daily TO nordabiz_app;
GRANT ALL ON TABLE hourly_activity TO nordabiz_app;
GRANT USAGE, SELECT ON SEQUENCE search_queries_id_seq TO nordabiz_app;
GRANT USAGE, SELECT ON SEQUENCE conversion_events_id_seq TO nordabiz_app;
GRANT USAGE, SELECT ON SEQUENCE js_errors_id_seq TO nordabiz_app;
GRANT USAGE, SELECT ON SEQUENCE popular_searches_daily_id_seq TO nordabiz_app;
GRANT USAGE, SELECT ON SEQUENCE hourly_activity_id_seq TO nordabiz_app;
COMMIT;
-- ============================================================
-- WERYFIKACJA
-- ============================================================
-- Sprawdź czy wszystkie tabele istnieją
DO $$
BEGIN
RAISE NOTICE 'Weryfikacja migracji 033_analytics_expansion:';
RAISE NOTICE '- search_queries: %', (SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'search_queries');
RAISE NOTICE '- conversion_events: %', (SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'conversion_events');
RAISE NOTICE '- js_errors: %', (SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'js_errors');
RAISE NOTICE '- popular_searches_daily: %', (SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'popular_searches_daily');
RAISE NOTICE '- hourly_activity: %', (SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'hourly_activity');
RAISE NOTICE 'Migracja 033 zakończona pomyślnie!';
END $$;