browser_commander/traces/
schema.rs1use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10
11pub const TRACE_SCHEMA_VERSION: u64 = 1;
16
17pub const TRACE_FORMAT: &str = "browser-commander-trace";
19
20pub const TRACE_EVENT_SOURCES: [&str; 7] = [
22 "navigation",
23 "interaction",
24 "console",
25 "pageerror",
26 "dialog",
27 "requestfailed",
28 "download",
29];
30
31pub struct TraceFiles;
33
34impl TraceFiles {
35 pub const MANIFEST: &'static str = "manifest.json";
37 pub const EVENTS: &'static str = "events.ndjson";
39 pub const CHECKPOINTS_DIR: &'static str = "checkpoints";
41 pub const MUTATIONS_DIR: &'static str = "mutations";
43 pub const ARTIFACTS_DIR: &'static str = "artifacts";
45 pub const VIEWER: &'static str = "viewer.html";
47}
48
49pub struct TraceMode;
51
52impl TraceMode {
53 pub const OFF: &'static str = "off";
55 pub const CHECKPOINTS: &'static str = "checkpoints";
57 pub const CONTINUOUS: &'static str = "continuous";
59 pub const RETAIN_ON_FAILURE: &'static str = "retain-on-failure";
61}
62
63pub struct TraceEvent;
65
66impl TraceEvent {
67 pub const TRACE_START: &'static str = "trace.start";
69 pub const TRACE_STOP: &'static str = "trace.stop";
71 pub const CHECKPOINT: &'static str = "checkpoint";
73 pub const MUTATIONS: &'static str = "mutations";
75 pub const NAVIGATION: &'static str = "navigation";
77 pub const INTERACTION: &'static str = "interaction";
79 pub const CONSOLE: &'static str = "console";
81 pub const PAGE_ERROR: &'static str = "pageerror";
83 pub const DIALOG: &'static str = "dialog";
85 pub const REQUEST_FAILED: &'static str = "requestfailed";
87 pub const DOWNLOAD: &'static str = "download";
89 pub const DROPPED: &'static str = "dropped";
91}
92
93pub struct TraceDropReason;
95
96impl TraceDropReason {
97 pub const SIZE_LIMIT: &'static str = "size-limit";
99 pub const WRITE_FAILED: &'static str = "write-failed";
101 pub const CAPTURE_FAILED: &'static str = "capture-failed";
103 pub const PAGE_CLOSED: &'static str = "page-closed";
105 pub const TIMEOUT: &'static str = "timeout";
107}
108
109pub struct TraceOutcome;
111
112impl TraceOutcome {
113 pub const COMPLETE: &'static str = "complete";
115 pub const PARTIAL: &'static str = "partial";
117 pub const TRUNCATED: &'static str = "truncated";
119}
120
121#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct TraceCounts {
125 #[serde(default)]
127 pub checkpoints: u64,
128 #[serde(default)]
130 pub events: u64,
131 #[serde(default)]
133 pub mutation_batches: u64,
134}
135
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub struct TraceManifest {
143 #[serde(default)]
145 pub schema_version: u64,
146 #[serde(default)]
148 pub format: String,
149 #[serde(default)]
151 pub mode: Option<String>,
152 #[serde(default = "complete_outcome")]
154 pub outcome: String,
155 #[serde(default)]
157 pub started_at: Option<String>,
158 #[serde(default)]
160 pub stopped_at: Option<String>,
161 #[serde(default)]
163 pub commander_version: Option<String>,
164 #[serde(default)]
166 pub engine: Option<String>,
167 #[serde(default)]
169 pub browser: Option<String>,
170 #[serde(default)]
172 pub platform: Option<String>,
173 #[serde(default)]
175 pub runtime: Option<String>,
176 #[serde(default)]
178 pub events: Vec<String>,
179 #[serde(default)]
181 pub dom: Map<String, Value>,
182 #[serde(default)]
184 pub privacy: Map<String, Value>,
185 #[serde(default)]
187 pub limits: Map<String, Value>,
188 #[serde(default)]
190 pub counts: TraceCounts,
191 #[serde(default)]
193 pub dropped: u64,
194 #[serde(flatten)]
196 pub extra: Map<String, Value>,
197}
198
199fn complete_outcome() -> String {
200 TraceOutcome::COMPLETE.to_string()
201}
202
203impl Default for TraceManifest {
204 fn default() -> Self {
205 Self {
206 schema_version: TRACE_SCHEMA_VERSION,
207 format: TRACE_FORMAT.to_string(),
208 mode: None,
209 outcome: complete_outcome(),
210 started_at: None,
211 stopped_at: None,
212 commander_version: None,
213 engine: None,
214 browser: None,
215 platform: Some(format!(
216 "{} {}",
217 std::env::consts::OS,
218 std::env::consts::ARCH
219 )),
220 runtime: Some(format!("rust {}", env!("CARGO_PKG_VERSION"))),
221 events: Vec::new(),
222 dom: Map::new(),
223 privacy: Map::new(),
224 limits: Map::new(),
225 counts: TraceCounts::default(),
226 dropped: 0,
227 extra: Map::new(),
228 }
229 }
230}
231
232impl TraceManifest {
233 pub fn is_complete(&self) -> bool {
235 self.outcome == TraceOutcome::COMPLETE
236 }
237}
238
239pub fn sequence_name(index: u32) -> String {
244 format!("{index:04}")
245}
246
247pub fn assert_readable_manifest(manifest: &TraceManifest) -> Result<(), crate::traces::TraceError> {
254 if manifest.format != TRACE_FORMAT {
255 return Err(crate::traces::TraceError::NotATrace);
256 }
257 if manifest.schema_version > TRACE_SCHEMA_VERSION {
258 return Err(crate::traces::TraceError::UnsupportedVersion {
259 found: manifest.schema_version,
260 });
261 }
262 Ok(())
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 fn manifest(format: &str, schema_version: u64) -> TraceManifest {
270 TraceManifest {
271 format: format.to_string(),
272 schema_version,
273 ..TraceManifest::default()
274 }
275 }
276
277 #[test]
278 fn pads_a_sequence_number_so_it_sorts_in_order() {
279 assert_eq!(sequence_name(1), "0001");
280 assert_eq!(sequence_name(42), "0042");
281 assert_eq!(sequence_name(10_000), "10000");
284 }
285
286 #[test]
287 fn accepts_a_manifest_this_reader_was_written_for() {
288 assert!(assert_readable_manifest(&manifest(TRACE_FORMAT, TRACE_SCHEMA_VERSION)).is_ok());
289 }
290
291 #[test]
292 fn refuses_something_that_is_not_a_trace() {
293 let error = assert_readable_manifest(&manifest("something-else", 1))
294 .expect_err("format does not match");
295
296 assert_eq!(error.to_string(), "not a Browser Commander trace bundle");
297 }
298
299 #[test]
300 fn refuses_a_newer_schema_rather_than_guessing() {
301 let error = assert_readable_manifest(&manifest(TRACE_FORMAT, TRACE_SCHEMA_VERSION + 1))
302 .expect_err("schema is too new");
303
304 assert!(error.to_string().contains("newer than this reader"));
305 }
306
307 #[test]
308 fn declares_the_same_schema_version_as_javascript() {
309 let source =
311 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../js/src/traces/schema.js");
312 let Ok(body) = std::fs::read_to_string(&source) else {
313 return;
315 };
316
317 assert!(body.contains(&format!("TRACE_SCHEMA_VERSION = {TRACE_SCHEMA_VERSION};")));
318 assert!(body.contains(&format!("format: '{TRACE_FORMAT}'")));
319 }
320}