browser_commander/traces/
schema.rs1use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10
11pub const TRACE_SCHEMA_VERSION: u64 = 2;
23
24pub const TRACE_FORMAT: &str = "browser-commander-trace";
26
27pub const TRACE_EVENT_SOURCES: [&str; 7] = [
29 "navigation",
30 "interaction",
31 "console",
32 "pageerror",
33 "dialog",
34 "requestfailed",
35 "download",
36];
37
38pub struct TraceFiles;
40
41impl TraceFiles {
42 pub const MANIFEST: &'static str = "manifest.json";
44 pub const EVENTS: &'static str = "events.ndjson";
46 pub const CHECKPOINTS_DIR: &'static str = "checkpoints";
48 pub const MUTATIONS_DIR: &'static str = "mutations";
50 pub const ARTIFACTS_DIR: &'static str = "artifacts";
52 pub const VIEWER: &'static str = "viewer.html";
54}
55
56pub struct TraceMode;
58
59impl TraceMode {
60 pub const OFF: &'static str = "off";
62 pub const CHECKPOINTS: &'static str = "checkpoints";
64 pub const CONTINUOUS: &'static str = "continuous";
66 pub const RETAIN_ON_FAILURE: &'static str = "retain-on-failure";
68}
69
70pub struct TraceEvent;
72
73impl TraceEvent {
74 pub const TRACE_START: &'static str = "trace.start";
76 pub const TRACE_STOP: &'static str = "trace.stop";
78 pub const CHECKPOINT: &'static str = "checkpoint";
80 pub const MUTATIONS: &'static str = "mutations";
82 pub const NAVIGATION: &'static str = "navigation";
84 pub const INTERACTION: &'static str = "interaction";
86 pub const CONSOLE: &'static str = "console";
88 pub const PAGE_ERROR: &'static str = "pageerror";
90 pub const DIALOG: &'static str = "dialog";
92 pub const REQUEST_FAILED: &'static str = "requestfailed";
94 pub const DOWNLOAD: &'static str = "download";
96 pub const DROPPED: &'static str = "dropped";
98}
99
100pub struct TraceMutationKind;
102
103impl TraceMutationKind {
104 pub const ATTRIBUTES: &'static str = "attributes";
106 pub const CHARACTER_DATA: &'static str = "characterData";
108 pub const CHILD_LIST: &'static str = "childList";
110 pub const LIVE_STATE: &'static str = "live-state";
116}
117
118pub struct TraceLiveState;
120
121impl TraceLiveState {
122 pub const VALUE: &'static str = "value";
124 pub const CHECKED: &'static str = "checked";
126 pub const SELECTED: &'static str = "selected";
128 pub const FOCUS: &'static str = "focus";
130 pub const SCROLL: &'static str = "scroll";
132}
133
134pub struct TraceCheckpointReason;
136
137impl TraceCheckpointReason {
138 pub const INITIAL: &'static str = "initial";
140 pub const CHECKPOINT: &'static str = "checkpoint";
142 pub const FAILURE: &'static str = "failure";
144}
145
146pub struct TraceDropReason;
148
149impl TraceDropReason {
150 pub const SIZE_LIMIT: &'static str = "size-limit";
152 pub const WRITE_FAILED: &'static str = "write-failed";
154 pub const CAPTURE_FAILED: &'static str = "capture-failed";
156 pub const PAGE_CLOSED: &'static str = "page-closed";
158 pub const TIMEOUT: &'static str = "timeout";
160}
161
162pub struct TraceOutcome;
164
165impl TraceOutcome {
166 pub const COMPLETE: &'static str = "complete";
168 pub const PARTIAL: &'static str = "partial";
170 pub const TRUNCATED: &'static str = "truncated";
172}
173
174#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
176#[serde(rename_all = "camelCase")]
177pub struct TraceCounts {
178 #[serde(default)]
180 pub checkpoints: u64,
181 #[serde(default)]
183 pub events: u64,
184 #[serde(default)]
186 pub mutation_batches: u64,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct TraceReplaySupport {
198 #[serde(default)]
200 pub checkpoints: bool,
201 #[serde(default)]
203 pub mutations: bool,
204 #[serde(default)]
206 pub child_list_positions: bool,
207 #[serde(default)]
209 pub live_state: bool,
210 #[serde(default)]
212 pub identifiers: bool,
213}
214
215impl Default for TraceReplaySupport {
216 fn default() -> Self {
217 Self {
218 checkpoints: true,
219 mutations: false,
220 child_list_positions: false,
221 live_state: false,
222 identifiers: false,
223 }
224 }
225}
226
227#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
232#[serde(rename_all = "camelCase")]
233pub struct TraceManifest {
234 #[serde(default)]
236 pub schema_version: u64,
237 #[serde(default)]
239 pub format: String,
240 #[serde(default)]
242 pub mode: Option<String>,
243 #[serde(default = "complete_outcome")]
245 pub outcome: String,
246 #[serde(default)]
248 pub started_at: Option<String>,
249 #[serde(default)]
251 pub stopped_at: Option<String>,
252 #[serde(default)]
254 pub commander_version: Option<String>,
255 #[serde(default)]
257 pub engine: Option<String>,
258 #[serde(default)]
260 pub browser: Option<String>,
261 #[serde(default)]
263 pub platform: Option<String>,
264 #[serde(default)]
266 pub runtime: Option<String>,
267 #[serde(default)]
269 pub events: Vec<String>,
270 #[serde(default)]
272 pub dom: Map<String, Value>,
273 #[serde(default)]
275 pub replay: TraceReplaySupport,
276 #[serde(default)]
278 pub privacy: Map<String, Value>,
279 #[serde(default)]
281 pub limits: Map<String, Value>,
282 #[serde(default)]
284 pub counts: TraceCounts,
285 #[serde(default)]
287 pub dropped: u64,
288 #[serde(flatten)]
290 pub extra: Map<String, Value>,
291}
292
293fn complete_outcome() -> String {
294 TraceOutcome::COMPLETE.to_string()
295}
296
297impl Default for TraceManifest {
298 fn default() -> Self {
299 Self {
300 schema_version: TRACE_SCHEMA_VERSION,
301 format: TRACE_FORMAT.to_string(),
302 mode: None,
303 outcome: complete_outcome(),
304 started_at: None,
305 stopped_at: None,
306 commander_version: None,
307 engine: None,
308 browser: None,
309 platform: Some(format!(
310 "{} {}",
311 std::env::consts::OS,
312 std::env::consts::ARCH
313 )),
314 runtime: Some(format!("rust {}", env!("CARGO_PKG_VERSION"))),
315 events: Vec::new(),
316 dom: Map::new(),
317 replay: TraceReplaySupport::default(),
318 privacy: Map::new(),
319 limits: Map::new(),
320 counts: TraceCounts::default(),
321 dropped: 0,
322 extra: Map::new(),
323 }
324 }
325}
326
327impl TraceManifest {
328 pub fn is_complete(&self) -> bool {
330 self.outcome == TraceOutcome::COMPLETE
331 }
332}
333
334pub fn sequence_name(index: u32) -> String {
339 format!("{index:04}")
340}
341
342pub fn assert_readable_manifest(manifest: &TraceManifest) -> Result<(), crate::traces::TraceError> {
349 if manifest.format != TRACE_FORMAT {
350 return Err(crate::traces::TraceError::NotATrace);
351 }
352 if manifest.schema_version > TRACE_SCHEMA_VERSION {
353 return Err(crate::traces::TraceError::UnsupportedVersion {
354 found: manifest.schema_version,
355 });
356 }
357 Ok(())
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 fn manifest(format: &str, schema_version: u64) -> TraceManifest {
365 TraceManifest {
366 format: format.to_string(),
367 schema_version,
368 ..TraceManifest::default()
369 }
370 }
371
372 #[test]
373 fn pads_a_sequence_number_so_it_sorts_in_order() {
374 assert_eq!(sequence_name(1), "0001");
375 assert_eq!(sequence_name(42), "0042");
376 assert_eq!(sequence_name(10_000), "10000");
379 }
380
381 #[test]
382 fn accepts_a_manifest_this_reader_was_written_for() {
383 assert!(assert_readable_manifest(&manifest(TRACE_FORMAT, TRACE_SCHEMA_VERSION)).is_ok());
384 }
385
386 #[test]
387 fn refuses_something_that_is_not_a_trace() {
388 let error = assert_readable_manifest(&manifest("something-else", 1))
389 .expect_err("format does not match");
390
391 assert_eq!(error.to_string(), "not a Browser Commander trace bundle");
392 }
393
394 #[test]
395 fn refuses_a_newer_schema_rather_than_guessing() {
396 let error = assert_readable_manifest(&manifest(TRACE_FORMAT, TRACE_SCHEMA_VERSION + 1))
397 .expect_err("schema is too new");
398
399 assert!(error.to_string().contains("newer than this reader"));
400 }
401
402 #[test]
403 fn declares_the_same_schema_version_as_javascript() {
404 let source =
406 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../js/src/traces/schema.js");
407 let Ok(body) = std::fs::read_to_string(&source) else {
408 return;
410 };
411
412 assert!(body.contains(&format!("TRACE_SCHEMA_VERSION = {TRACE_SCHEMA_VERSION};")));
413 assert!(body.contains(&format!("format: '{TRACE_FORMAT}'")));
414 }
415}