-- ============================================================ -- 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 $$;