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#[allow(clippy::large_enum_variant)]
53#[derive(Debug, Serialize)]
54#[serde(tag = "event", rename_all = "snake_case")]
55pub enum AnalyticsEvent {
56 ClientConnected {
57 ts: String,
58 username: String,
59 platform: String,
60 #[serde(skip_serializing_if = "Option::is_none")]
64 version: Option<String>,
65 reconnected: bool,
66 },
67 GameStarted {
68 ts: String,
69 game_id: String,
70 room_id: String,
71 format: GameFormat,
72 engine: EngineKind,
73 hosted: bool,
74 official: bool,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 table_style: Option<String>,
77 starting_life: i32,
78 players: Vec<SeatInfo>,
79 },
80 GameEnded {
81 ts: String,
82 game_id: String,
83 room_id: String,
84 reason: GameEndReason,
85 duration_s: f64,
86 game_over: bool,
87 winner: Option<String>,
88 conceded: Vec<String>,
89 fatal_message: Option<String>,
90 reported: bool,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 turns: Option<u32>,
95 },
96 TransportUsed {
98 ts: String,
99 room_id: String,
100 game_id: String,
101 host: String,
102 seats: Vec<SeatTransportReport>,
103 },
104 PlaneQuality {
106 ts: String,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 room_id: Option<String>,
109 username: String,
111 peer: String,
113 plane: String,
114 outcome: String,
115 phase: String,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 connect_ms: Option<u32>,
119 #[serde(skip_serializing_if = "Option::is_none")]
120 rtt_ms: Option<u32>,
121 #[serde(skip_serializing_if = "Option::is_none")]
122 relay_rtt_ms: Option<u32>,
123 #[serde(skip_serializing_if = "Option::is_none")]
124 candidate_pair: Option<String>,
125 },
126 EngineStats {
127 ts: String,
128 #[serde(skip_serializing_if = "Option::is_none")]
132 room_id: Option<String>,
133 username: String,
134 report_id: String,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 game_id: Option<String>,
137 engine: String,
138 client_version: String,
139 platform: String,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 format: Option<String>,
142 seats: u32,
143 multiplayer: bool,
144 duration_s: u32,
145 end_reason: String,
146 decisions: u32,
147 turnaround_p50: u32,
148 turnaround_p90: u32,
149 turnaround_max: u32,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 engine_p50: Option<u32>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 engine_p90: Option<u32>,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 engine_max: Option<u32>,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 engine_same_p50: Option<u32>,
158 #[serde(skip_serializing_if = "Option::is_none")]
159 engine_same_p90: Option<u32>,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 engine_same_max: Option<u32>,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 engine_cross_p50: Option<u32>,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 engine_cross_p90: Option<u32>,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 engine_cross_max: Option<u32>,
168 think_hidden: u32,
169 #[serde(skip_serializing_if = "Option::is_none")]
173 reply_wait_p50: Option<u32>,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 reply_wait_p90: Option<u32>,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 reply_wait_max: Option<u32>,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 client_work_p50: Option<u32>,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 client_work_p90: Option<u32>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 client_work_max: Option<u32>,
184 #[serde(skip_serializing_if = "Option::is_none")]
188 engine_bot_p50: Option<u32>,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 engine_bot_p90: Option<u32>,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 engine_bot_max: Option<u32>,
193 #[serde(skip_serializing_if = "Option::is_none")]
194 engine_rules_p50: Option<u32>,
195 #[serde(skip_serializing_if = "Option::is_none")]
196 engine_rules_p90: Option<u32>,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 engine_rules_max: Option<u32>,
199 },
200
201 DeckSelected {
202 ts: String,
203 room_id: String,
204 username: String,
205 is_bot: bool,
206 deck_name: String,
207 commander: Option<String>,
208 cards: Vec<CardEntry>,
209 sideboard_count: usize,
210 },
211 SeatJoined {
212 ts: String,
213 room_id: String,
214 username: String,
215 is_bot: bool,
216 observer: bool,
217 },
218 SeatLeft {
219 ts: String,
220 room_id: String,
221 username: String,
222 },
223}
224
225#[derive(Serialize)]
226pub struct CaptureLine<'a> {
227 pub ts: String,
228 pub from: &'a str,
229 pub envelope: &'a Value,
230 #[serde(rename = "clientRttMs", skip_serializing_if = "Option::is_none")]
234 pub client_rtt_ms: Option<u32>,
235}