manabrew_server/analytics/
event.rs1use serde::Serialize;
2use serde_json::Value;
3
4use crate::protocol::{EngineKind, GameFormat, SeatTransportReport};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
7#[serde(rename_all = "snake_case")]
8pub enum GameEndReason {
9 GameOver,
10 EngineFatal,
11 HostEnded,
12 ReconnectTimeout,
13 HostLost,
14 StaleExpired,
15 Abandoned,
16}
17
18impl GameEndReason {
19 pub fn as_str(self) -> &'static str {
20 match self {
21 Self::GameOver => "game_over",
22 Self::EngineFatal => "engine_fatal",
23 Self::HostEnded => "host_ended",
24 Self::ReconnectTimeout => "reconnect_timeout",
25 Self::HostLost => "host_lost",
26 Self::StaleExpired => "stale_expired",
27 Self::Abandoned => "abandoned",
28 }
29 }
30}
31
32#[derive(Debug, Serialize)]
33pub struct SeatInfo {
34 pub username: String,
35 pub is_bot: bool,
36 pub deck_name: Option<String>,
37 pub commander: Option<String>,
38 pub published_deck_id: Option<String>,
39 pub deck_fingerprint: Option<String>,
40}
41
42#[derive(Debug, Serialize)]
43pub struct CardEntry {
44 pub name: String,
45 pub set_code: String,
46 pub count: u32,
47}
48
49#[derive(Debug, Serialize)]
50#[serde(tag = "event", rename_all = "snake_case")]
51pub enum AnalyticsEvent {
52 ClientConnected {
53 ts: String,
54 username: String,
55 platform: String,
56 #[serde(skip_serializing_if = "Option::is_none")]
60 version: Option<String>,
61 reconnected: bool,
62 },
63 GameStarted {
64 ts: String,
65 game_id: String,
66 room_id: String,
67 format: GameFormat,
68 engine: EngineKind,
69 hosted: bool,
70 official: bool,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 table_style: Option<String>,
73 starting_life: i32,
74 players: Vec<SeatInfo>,
75 },
76 GameEnded {
77 ts: String,
78 game_id: String,
79 room_id: String,
80 reason: GameEndReason,
81 duration_s: f64,
82 game_over: bool,
83 winner: Option<String>,
84 conceded: Vec<String>,
85 fatal_message: Option<String>,
86 reported: bool,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 turns: Option<u32>,
91 },
92 TransportUsed {
94 ts: String,
95 room_id: String,
96 game_id: String,
97 host: String,
98 seats: Vec<SeatTransportReport>,
99 },
100 PlaneQuality {
102 ts: String,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 room_id: Option<String>,
105 username: String,
107 peer: String,
109 plane: String,
110 outcome: String,
111 phase: String,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 connect_ms: Option<u32>,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 rtt_ms: Option<u32>,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 relay_rtt_ms: Option<u32>,
119 #[serde(skip_serializing_if = "Option::is_none")]
120 candidate_pair: Option<String>,
121 },
122 EngineStats {
123 ts: String,
124 #[serde(skip_serializing_if = "Option::is_none")]
128 room_id: Option<String>,
129 username: String,
130 report_id: String,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 game_id: Option<String>,
133 engine: String,
134 client_version: String,
135 platform: String,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 format: Option<String>,
138 seats: u32,
139 multiplayer: bool,
140 duration_s: u32,
141 end_reason: String,
142 decisions: u32,
143 turnaround_p50: u32,
144 turnaround_p90: u32,
145 turnaround_max: u32,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 engine_p50: Option<u32>,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 engine_p90: Option<u32>,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 engine_max: Option<u32>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 engine_same_p50: Option<u32>,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 engine_same_p90: Option<u32>,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 engine_same_max: Option<u32>,
158 #[serde(skip_serializing_if = "Option::is_none")]
159 engine_cross_p50: Option<u32>,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 engine_cross_p90: Option<u32>,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 engine_cross_max: Option<u32>,
164 think_hidden: u32,
165 },
166
167 DeckSelected {
168 ts: String,
169 room_id: String,
170 username: String,
171 is_bot: bool,
172 deck_name: String,
173 commander: Option<String>,
174 cards: Vec<CardEntry>,
175 sideboard_count: usize,
176 },
177 SeatJoined {
178 ts: String,
179 room_id: String,
180 username: String,
181 is_bot: bool,
182 observer: bool,
183 },
184 SeatLeft {
185 ts: String,
186 room_id: String,
187 username: String,
188 },
189}
190
191#[derive(Serialize)]
192pub struct CaptureLine<'a> {
193 pub ts: String,
194 pub from: &'a str,
195 pub envelope: &'a Value,
196 #[serde(rename = "clientRttMs", skip_serializing_if = "Option::is_none")]
200 pub client_rtt_ms: Option<u32>,
201}