use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
pub const TRACE_SCHEMA_VERSION: u64 = 1;
pub const TRACE_FORMAT: &str = "browser-commander-trace";
pub const TRACE_EVENT_SOURCES: [&str; 7] = [
"navigation",
"interaction",
"console",
"pageerror",
"dialog",
"requestfailed",
"download",
];
pub struct TraceFiles;
impl TraceFiles {
pub const MANIFEST: &'static str = "manifest.json";
pub const EVENTS: &'static str = "events.ndjson";
pub const CHECKPOINTS_DIR: &'static str = "checkpoints";
pub const MUTATIONS_DIR: &'static str = "mutations";
pub const ARTIFACTS_DIR: &'static str = "artifacts";
pub const VIEWER: &'static str = "viewer.html";
}
pub struct TraceMode;
impl TraceMode {
pub const OFF: &'static str = "off";
pub const CHECKPOINTS: &'static str = "checkpoints";
pub const CONTINUOUS: &'static str = "continuous";
pub const RETAIN_ON_FAILURE: &'static str = "retain-on-failure";
}
pub struct TraceEvent;
impl TraceEvent {
pub const TRACE_START: &'static str = "trace.start";
pub const TRACE_STOP: &'static str = "trace.stop";
pub const CHECKPOINT: &'static str = "checkpoint";
pub const MUTATIONS: &'static str = "mutations";
pub const NAVIGATION: &'static str = "navigation";
pub const INTERACTION: &'static str = "interaction";
pub const CONSOLE: &'static str = "console";
pub const PAGE_ERROR: &'static str = "pageerror";
pub const DIALOG: &'static str = "dialog";
pub const REQUEST_FAILED: &'static str = "requestfailed";
pub const DOWNLOAD: &'static str = "download";
pub const DROPPED: &'static str = "dropped";
}
pub struct TraceDropReason;
impl TraceDropReason {
pub const SIZE_LIMIT: &'static str = "size-limit";
pub const WRITE_FAILED: &'static str = "write-failed";
pub const CAPTURE_FAILED: &'static str = "capture-failed";
pub const PAGE_CLOSED: &'static str = "page-closed";
pub const TIMEOUT: &'static str = "timeout";
}
pub struct TraceOutcome;
impl TraceOutcome {
pub const COMPLETE: &'static str = "complete";
pub const PARTIAL: &'static str = "partial";
pub const TRUNCATED: &'static str = "truncated";
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TraceCounts {
#[serde(default)]
pub checkpoints: u64,
#[serde(default)]
pub events: u64,
#[serde(default)]
pub mutation_batches: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TraceManifest {
#[serde(default)]
pub schema_version: u64,
#[serde(default)]
pub format: String,
#[serde(default)]
pub mode: Option<String>,
#[serde(default = "complete_outcome")]
pub outcome: String,
#[serde(default)]
pub started_at: Option<String>,
#[serde(default)]
pub stopped_at: Option<String>,
#[serde(default)]
pub commander_version: Option<String>,
#[serde(default)]
pub engine: Option<String>,
#[serde(default)]
pub browser: Option<String>,
#[serde(default)]
pub platform: Option<String>,
#[serde(default)]
pub runtime: Option<String>,
#[serde(default)]
pub events: Vec<String>,
#[serde(default)]
pub dom: Map<String, Value>,
#[serde(default)]
pub privacy: Map<String, Value>,
#[serde(default)]
pub limits: Map<String, Value>,
#[serde(default)]
pub counts: TraceCounts,
#[serde(default)]
pub dropped: u64,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
fn complete_outcome() -> String {
TraceOutcome::COMPLETE.to_string()
}
impl Default for TraceManifest {
fn default() -> Self {
Self {
schema_version: TRACE_SCHEMA_VERSION,
format: TRACE_FORMAT.to_string(),
mode: None,
outcome: complete_outcome(),
started_at: None,
stopped_at: None,
commander_version: None,
engine: None,
browser: None,
platform: Some(format!(
"{} {}",
std::env::consts::OS,
std::env::consts::ARCH
)),
runtime: Some(format!("rust {}", env!("CARGO_PKG_VERSION"))),
events: Vec::new(),
dom: Map::new(),
privacy: Map::new(),
limits: Map::new(),
counts: TraceCounts::default(),
dropped: 0,
extra: Map::new(),
}
}
}
impl TraceManifest {
pub fn is_complete(&self) -> bool {
self.outcome == TraceOutcome::COMPLETE
}
}
pub fn sequence_name(index: u32) -> String {
format!("{index:04}")
}
pub fn assert_readable_manifest(manifest: &TraceManifest) -> Result<(), crate::traces::TraceError> {
if manifest.format != TRACE_FORMAT {
return Err(crate::traces::TraceError::NotATrace);
}
if manifest.schema_version > TRACE_SCHEMA_VERSION {
return Err(crate::traces::TraceError::UnsupportedVersion {
found: manifest.schema_version,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn manifest(format: &str, schema_version: u64) -> TraceManifest {
TraceManifest {
format: format.to_string(),
schema_version,
..TraceManifest::default()
}
}
#[test]
fn pads_a_sequence_number_so_it_sorts_in_order() {
assert_eq!(sequence_name(1), "0001");
assert_eq!(sequence_name(42), "0042");
assert_eq!(sequence_name(10_000), "10000");
}
#[test]
fn accepts_a_manifest_this_reader_was_written_for() {
assert!(assert_readable_manifest(&manifest(TRACE_FORMAT, TRACE_SCHEMA_VERSION)).is_ok());
}
#[test]
fn refuses_something_that_is_not_a_trace() {
let error = assert_readable_manifest(&manifest("something-else", 1))
.expect_err("format does not match");
assert_eq!(error.to_string(), "not a Browser Commander trace bundle");
}
#[test]
fn refuses_a_newer_schema_rather_than_guessing() {
let error = assert_readable_manifest(&manifest(TRACE_FORMAT, TRACE_SCHEMA_VERSION + 1))
.expect_err("schema is too new");
assert!(error.to_string().contains("newer than this reader"));
}
#[test]
fn declares_the_same_schema_version_as_javascript() {
let source =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../js/src/traces/schema.js");
let Ok(body) = std::fs::read_to_string(&source) else {
return;
};
assert!(body.contains(&format!("TRACE_SCHEMA_VERSION = {TRACE_SCHEMA_VERSION};")));
assert!(body.contains(&format!("format: '{TRACE_FORMAT}'")));
}
}