feat: Implement Player Retention Analytics System (Phase 2.2)

Backend:
- Add player_sessions table (migration 004) for session tracking
- Implement retention calculation queries (24h/48h/72h post-wipe)
- Add /api/plugin/player-event endpoint for join/leave tracking
- Add /api/analytics/retention endpoint with CSV export
- Track unique players, session duration, new vs returning ratio

Frontend:
- Create PlayerRetentionView with ECharts retention curves
- Add multi-wipe comparison (last 3/6/10/20 wipes)
- Display summary metrics and detailed wipe table
- Add /retention route to router

Plugin:
- Update CorrosionCompanion.cs to send player events to new endpoint
- Track player join/leave with license_key authentication

Enables data-driven wipe timing optimization by answering:
"What percentage of players return 24h/48h/72h after a wipe?"

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-02-15 14:23:21 -05:00
parent cef89ade18
commit f29524e633
9 changed files with 1020 additions and 12 deletions

View File

@@ -107,19 +107,27 @@ namespace Oxide.Plugins
var data = new Dictionary<string, object>
{
{ "license_key", config.LicenseKey },
{ "event", "player_connected" },
{ "player_id", player.UserIDString },
{ "player_name", player.displayName },
{ "ip_address", player.net?.connection?.ipaddress ?? "unknown" },
{ "timestamp", DateTimeOffset.UtcNow.ToUnixTimeSeconds() }
};
SendEvent("player_connected", data);
if (config.DebugMode)
SendApiRequest("/api/plugin/player-event", data, (code, response) =>
{
Puts($"Player connected: {player.displayName} ({player.UserIDString})");
}
if (config.DebugMode)
{
if (code == 200)
{
Puts($"Player join tracked: {player.displayName} ({player.UserIDString})");
}
else
{
PrintWarning($"Player join tracking failed: HTTP {code}");
}
}
});
}
void OnPlayerDisconnected(BasePlayer player, string reason)
@@ -128,19 +136,27 @@ namespace Oxide.Plugins
var data = new Dictionary<string, object>
{
{ "license_key", config.LicenseKey },
{ "event", "player_disconnected" },
{ "player_id", player.UserIDString },
{ "player_name", player.displayName },
{ "reason", reason ?? "unknown" },
{ "timestamp", DateTimeOffset.UtcNow.ToUnixTimeSeconds() }
};
SendEvent("player_disconnected", data);
if (config.DebugMode)
SendApiRequest("/api/plugin/player-event", data, (code, response) =>
{
Puts($"Player disconnected: {player.displayName} (Reason: {reason})");
}
if (config.DebugMode)
{
if (code == 200)
{
Puts($"Player leave tracked: {player.displayName} (Reason: {reason})");
}
else
{
PrintWarning($"Player leave tracking failed: HTTP {code}");
}
}
});
}
object OnPlayerChat(BasePlayer player, string message)