Player Activity Tracking¶
The player-activity layer is dual-layer: Redis holds hot, TTL-expiring session/event/summary data for online presence and live counters; selected session-boundary and trade events also write durable Postgres rows (PlayerSession / PlayerActivity) that retention and region-activity jobs read.
Implemented in services/gameserver/src/services/player_activity_service.py (PlayerActivityService), reached through the module-level singleton accessor get_player_activity_service(). Each method lazily resolves the shared RedisService. Durable mirrors accept an optional sync SQLAlchemy Session (db=) because production callers (auth.py, trading.py) already hold sync sessions from get_db.
What is tracked¶
Two kinds of writes flow through the service:
- Session lifecycle —
track_loginopens a session;track_logoutfinalises it, computingduration_secondsand folding the session into the rolling and daily summaries. Login and logout are wired into the auth routes (services/gameserver/src/api/routes/auth.py), mapping the authenticated user to itsPlayerrow; admins and non-player users are skipped, and any tracking failure is swallowed so it can never break auth. Whendbis passed (auth does), login/logout also mirror into Postgres (below). - Gameplay events —
track_activity(player_id, event_type, details, db=None)increments the live session counters and appends an event record. The recognised event types (ActivityEventType) arelogin,logout,trade_buy,trade_sell,combat_attack,combat_defend,sector_move,dock,undock,planet_land, andwarp. Trade events add totrades_countandtrade_volume(fromdetails["total_value"]); combat events add tocombat_events;sector_moveappends to a deduplicatedsectors_visitedlist capped at the last 100 entries. Whendbis passed fortrade_buy/trade_sell(trading routes do), those events also insert durablePlayerActivityrows.
Every write also pushes an individual event onto the player's Redis event list (_record_event), trimmed to the most recent 500 entries.
Redis keys and TTLs¶
| Key | Contents | TTL |
|---|---|---|
activity:session:{player_id} |
Current session: login/last-activity timestamps and live counters | 24 hours |
activity:events:{player_id} |
Most recent events (list, capped at 500) | 7 days |
activity:summary:{player_id} |
Rolling per-player summary: total sessions, playtime, actions, trades, trade volume, combat events, unique sectors | 30 days |
activity:daily:{player_id}:{date} |
Per-day aggregates (UTC date) | 14 days |
activity:online_players |
Set of currently-online player IDs | no TTL (managed by add on login / remove on logout) |
The session key is added to activity:online_players on login and removed on logout; get_online_player_count and get_online_player_ids read that set. The session key itself is deleted on logout.
Persistence — dual-layer (Redis hot + durable SQL)¶
Redis hot path¶
Session counters, event lists, rolling/daily summaries, and the online-player set live in Redis and are not durable. Counters and summaries are written with SETEX (RedisService.cache_set), so they expire on the TTLs above and are lost on a Redis flush or data loss.
Consequences for the Redis layer:
- The online-player set, session counters, summaries, and per-day aggregates reset to zero on Redis loss.
- The longest-lived Redis record is the 30-day rolling summary; per-day aggregates persist 14 days, raw events 7 days, and the live session 24 hours.
- Read accessors (
get_player_session,get_player_summary,get_recent_events,get_daily_stats) return zeroed defaults when the corresponding key has expired or never existed, rather than erroring.
Durable Postgres writebacks¶
When callers pass a sync db session, the service also mirrors into tables defined in services/gameserver/src/models/player_analytics.py (best-effort / non-fatal — failures are logged and rolled back or savepoint-isolated so they cannot break auth or trade):
| Path | Durable write | Notes |
|---|---|---|
track_login(..., db=) |
Updates Player.last_game_login; opens PlayerSession; inserts PlayerActivity type login; stashes db_session_id on the Redis session dict |
Wired from auth.py |
track_logout(..., db=) |
Completes matching PlayerSession (end_time, duration_minutes, actions/sectors); inserts PlayerActivity type logout |
Requires db_session_id from login; wired from auth.py |
track_activity(..., trade_buy\|trade_sell, db=) |
Inserts PlayerActivity with credits_involved (and optional items_involved) |
Wired from trading.py; uses a savepoint so a failed insert cannot poison the trade transaction (WO-BUILD-RETENTION-SIGNALS-TRADE-SQL-INSERT / e42d6c6e) |
These durable rows feed RetentionService at-risk signals (declining_session_length, early_logout_streak, economic_loss_streak) and the WO-G18 Region.active_players_30d recompute. See OPERATIONS/retention.md.
There is no separate async “drain Redis → Postgres every N minutes” job on this path today — durability is the synchronous db= writebacks above.
Event types that remain Redis-only¶
These ActivityEventType values update Redis session counters / event lists only; they do not insert PlayerActivity rows even when db is available:
combat_attackcombat_defendsector_movedockundockplanet_landwarp
trade_buy / trade_sell called without db are also Redis-only for that call. Residual undercount for region/retention aggregates: players whose only activity in the window is one of the Redis-only types above, or durable rows with sector_id NULL (they fail the region join).
Source map¶
| Concern | Path |
|---|---|
| Activity service | services/gameserver/src/services/player_activity_service.py |
Redis cache primitives (cache_set/cache_get/cache_delete) |
services/gameserver/src/services/redis_service.py |
| Login / logout wiring | services/gameserver/src/api/routes/auth.py |
Trade wiring (trade_buy / trade_sell + db=) |
services/gameserver/src/api/routes/trading.py |
| Durable models | services/gameserver/src/models/player_analytics.py (PlayerSession, PlayerActivity, PlayerAnalyticsSnapshot) |
| Durable last-login column | services/gameserver/src/models/player.py (Player.last_game_login) |
Related¶
OPERATIONS/retention.md— engagement metrics and the wider retention machinery that consume durable activity rows and Redis online presence.OPERATIONS/realtime.md— presence and the Redis-backed real-time layer.