feat: add user engagement tracking (login_count, last_active_at, page_views_count)
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ace9c15cd7
commit
825c79c399
17
app.py
17
app.py
@ -617,10 +617,12 @@ def check_geoip():
|
|||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def update_last_active():
|
def update_last_active():
|
||||||
"""Update last_login periodically (every 5 min) so profile shows fresh activity."""
|
"""Update last_active_at and page_views_count for engagement tracking."""
|
||||||
if not current_user.is_authenticated:
|
if not current_user.is_authenticated:
|
||||||
return
|
return
|
||||||
if request.path.startswith('/static') or request.path == '/health':
|
if request.path.startswith('/static') or request.path == '/health' or request.path == '/favicon.ico':
|
||||||
|
return
|
||||||
|
if request.path.startswith('/api'):
|
||||||
return
|
return
|
||||||
from flask import session as flask_session
|
from flask import session as flask_session
|
||||||
import time
|
import time
|
||||||
@ -629,11 +631,15 @@ def update_last_active():
|
|||||||
if now - last_update < 300: # 5 minutes
|
if now - last_update < 300: # 5 minutes
|
||||||
return
|
return
|
||||||
flask_session['_last_active_update'] = now
|
flask_session['_last_active_update'] = now
|
||||||
|
# Count page views in session, flush to DB every 5 min
|
||||||
|
pv = flask_session.get('_pv_buffer', 0)
|
||||||
|
flask_session['_pv_buffer'] = 0
|
||||||
try:
|
try:
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
user = db.query(User).filter_by(id=current_user.id).first()
|
user = db.query(User).filter_by(id=current_user.id).first()
|
||||||
if user:
|
if user:
|
||||||
user.last_login = datetime.now()
|
user.last_active_at = datetime.now()
|
||||||
|
user.page_views_count = (user.page_views_count or 0) + pv
|
||||||
db.commit()
|
db.commit()
|
||||||
db.close()
|
db.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -663,6 +669,11 @@ def track_page_view():
|
|||||||
if request.path == '/favicon.ico':
|
if request.path == '/favicon.ico':
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Buffer page views for authenticated users (flushed in update_last_active)
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
from flask import session as flask_session
|
||||||
|
flask_session['_pv_buffer'] = flask_session.get('_pv_buffer', 0) + 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
session_db_id = get_or_create_analytics_session()
|
session_db_id = get_or_create_analytics_session()
|
||||||
if not session_db_id:
|
if not session_db_id:
|
||||||
|
|||||||
@ -380,6 +380,7 @@ def login():
|
|||||||
# No 2FA - login directly
|
# No 2FA - login directly
|
||||||
login_user(user, remember=remember)
|
login_user(user, remember=remember)
|
||||||
user.last_login = datetime.now()
|
user.last_login = datetime.now()
|
||||||
|
user.login_count = (user.login_count or 0) + 1
|
||||||
_auto_link_person(db, user)
|
_auto_link_person(db, user)
|
||||||
|
|
||||||
# Log successful login to audit
|
# Log successful login to audit
|
||||||
@ -478,6 +479,7 @@ def verify_2fa():
|
|||||||
login_user(user, remember=remember)
|
login_user(user, remember=remember)
|
||||||
session['2fa_verified'] = True
|
session['2fa_verified'] = True
|
||||||
user.last_login = datetime.now()
|
user.last_login = datetime.now()
|
||||||
|
user.login_count = (user.login_count or 0) + 1
|
||||||
_auto_link_person(db, user)
|
_auto_link_person(db, user)
|
||||||
|
|
||||||
# Log successful 2FA login to audit
|
# Log successful 2FA login to audit
|
||||||
@ -1251,6 +1253,7 @@ def verify_email(token):
|
|||||||
if not current_user.is_authenticated:
|
if not current_user.is_authenticated:
|
||||||
login_user(user, remember=True)
|
login_user(user, remember=True)
|
||||||
user.last_login = datetime.now()
|
user.last_login = datetime.now()
|
||||||
|
user.login_count = (user.login_count or 0) + 1
|
||||||
db.commit()
|
db.commit()
|
||||||
flash('Witamy ponownie! Zostales automatycznie zalogowany.', 'info')
|
flash('Witamy ponownie! Zostales automatycznie zalogowany.', 'info')
|
||||||
return redirect(url_for('dashboard'))
|
return redirect(url_for('dashboard'))
|
||||||
@ -1265,6 +1268,7 @@ def verify_email(token):
|
|||||||
# Auto-login the user after verification
|
# Auto-login the user after verification
|
||||||
login_user(user, remember=True)
|
login_user(user, remember=True)
|
||||||
user.last_login = datetime.now()
|
user.last_login = datetime.now()
|
||||||
|
user.login_count = (user.login_count or 0) + 1
|
||||||
|
|
||||||
# Log successful verification and login
|
# Log successful verification and login
|
||||||
client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
|
client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
|
||||||
|
|||||||
@ -298,8 +298,13 @@ class User(Base, UserMixin):
|
|||||||
# Timestamps
|
# Timestamps
|
||||||
created_at = Column(DateTime, default=datetime.now)
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
last_login = Column(DateTime)
|
last_login = Column(DateTime)
|
||||||
|
last_active_at = Column(DateTime)
|
||||||
verified_at = Column(DateTime)
|
verified_at = Column(DateTime)
|
||||||
|
|
||||||
|
# Engagement metrics
|
||||||
|
login_count = Column(Integer, default=0)
|
||||||
|
page_views_count = Column(Integer, default=0)
|
||||||
|
|
||||||
# Verification token
|
# Verification token
|
||||||
verification_token = Column(String(255))
|
verification_token = Column(String(255))
|
||||||
verification_token_expires = Column(DateTime)
|
verification_token_expires = Column(DateTime)
|
||||||
|
|||||||
20
database/migrations/add_user_engagement_metrics.sql
Normal file
20
database/migrations/add_user_engagement_metrics.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
-- Add user engagement tracking columns
|
||||||
|
-- 2026-03-13
|
||||||
|
|
||||||
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS login_count INTEGER DEFAULT 0;
|
||||||
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_active_at TIMESTAMP;
|
||||||
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS page_views_count INTEGER DEFAULT 0;
|
||||||
|
|
||||||
|
-- Backfill login_count from audit_logs
|
||||||
|
UPDATE users u
|
||||||
|
SET login_count = COALESCE(sub.cnt, 0)
|
||||||
|
FROM (
|
||||||
|
SELECT user_id, COUNT(*) as cnt
|
||||||
|
FROM audit_logs
|
||||||
|
WHERE action = 'login'
|
||||||
|
GROUP BY user_id
|
||||||
|
) sub
|
||||||
|
WHERE u.id = sub.user_id;
|
||||||
|
|
||||||
|
-- Backfill last_active_at from last_login
|
||||||
|
UPDATE users SET last_active_at = last_login WHERE last_login IS NOT NULL;
|
||||||
Loading…
Reference in New Issue
Block a user