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