use std::cmp::Ordering;
use super::assets::trace_assets;
use super::jsonfmt::{Json, JsonObject};
use super::page::{with_deadline, TracePage};
pub const DEFAULT_MAX_QUEUED_MUTATIONS: u64 = 5000;
#[derive(Debug, Clone, Default, PartialEq)]
pub(crate) struct Drained {
pub batches: Vec<Json>,
pub over: f64,
pub frames: usize,
}
#[derive(Debug, Clone)]
pub(crate) struct MutationStream {
enabled: bool,
recorder_options: Json,
capture_timeout_ms: u64,
}
impl MutationStream {
pub fn new(
enabled: bool,
redact_selectors: &[String],
max_queued: Option<u64>,
live_state: bool,
capture_timeout_ms: u64,
) -> Self {
let assets = trace_assets();
let selectors = redact_selectors.iter().map(Json::from).collect::<Vec<_>>();
let recorder_options = JsonObject::new()
.with("globalName", assets.recorder_global.as_str())
.with("redactSelectors", selectors)
.with("redacted", assets.redacted.as_str())
.with(
"maxQueued",
max_queued.unwrap_or(DEFAULT_MAX_QUEUED_MUTATIONS),
)
.with("liveState", live_state);
Self {
enabled,
recorder_options: Json::Object(recorder_options),
capture_timeout_ms,
}
}
async fn evaluate_in_frames(
page: &dyn TracePage,
source: &str,
argument: &Json,
) -> Result<Vec<Json>, String> {
Ok(vec![page.evaluate_function(source, argument).await?])
}
pub async fn install(&self, page: &dyn TracePage) -> Result<(), String> {
if !self.enabled {
return Ok(());
}
let source = &trace_assets().capture.install_mutation_recorder;
Self::evaluate_in_frames(page, source, &self.recorder_options)
.await
.map(|_| ())
}
pub async fn install_persistent(&self, page: &dyn TracePage) -> Result<Option<String>, String> {
if !self.enabled {
return Ok(None);
}
let source = &trace_assets().capture.install_mutation_recorder;
page.add_init_script(source, &self.recorder_options).await
}
pub async fn drain(&self, page: &dyn TracePage) -> Result<Option<Vec<Json>>, String> {
if !self.enabled {
return Ok(None);
}
let assets = trace_assets();
let global = Json::from(assets.recorder_global.as_str());
with_deadline(
Self::evaluate_in_frames(page, &assets.capture.drain_mutations, &global),
self.capture_timeout_ms,
"trace mutation drain",
)
.await
.map(Some)
}
pub async fn stop(&self, page: &dyn TracePage) -> Result<(), String> {
if !self.enabled {
return Ok(());
}
let assets = trace_assets();
let global = Json::from(assets.recorder_global.as_str());
Self::evaluate_in_frames(page, &assets.capture.stop_mutation_recorder, &global)
.await
.map(|_| ())
}
}
fn queued_at(batch: &Json) -> f64 {
batch.get("at").and_then(Json::as_f64).unwrap_or(0.0)
}
pub(crate) fn collect_batches(drained: &[Json], owner: &JsonObject) -> Drained {
let mut batches = Vec::new();
let mut over = 0.0;
for frame in drained {
over += frame.get("dropped").and_then(Json::as_f64).unwrap_or(0.0);
let frame_batches = frame.get("batches").and_then(Json::as_array);
for batch in frame_batches.into_iter().flatten() {
let mut record = owner.clone();
if let Some(fields) = batch.as_object() {
record.extend_from(fields);
}
batches.push(Json::Object(record));
}
}
batches.sort_by(|left, right| {
queued_at(left)
.partial_cmp(&queued_at(right))
.unwrap_or(Ordering::Equal)
});
Drained {
batches,
over,
frames: drained.len(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn batches_are_owned_and_ordered() {
let frame = Json::parse(
r#"{"batches":[{"at":2,"records":[]},{"navigationId":"nav-9","at":1}],"dropped":3}"#,
)
.unwrap();
let owner = JsonObject::new()
.with("traceId", "trace-1")
.with("navigationId", "nav-1");
let drained = collect_batches(&[frame], &owner);
assert_eq!(drained.over, 3.0);
assert_eq!(drained.frames, 1);
assert_eq!(
drained
.batches
.iter()
.map(Json::to_compact)
.collect::<Vec<_>>(),
vec![
r#"{"traceId":"trace-1","navigationId":"nav-9","at":1}"#,
r#"{"traceId":"trace-1","navigationId":"nav-1","at":2,"records":[]}"#,
]
);
}
#[test]
fn the_recorder_options_match_javascript() {
let stream = MutationStream::new(true, &["[data-private]".to_string()], None, true, 0);
assert_eq!(
stream.recorder_options.to_compact(),
r#"{"globalName":"__browserCommanderTrace__","redactSelectors":["[data-private]"],"redacted":"[redacted]","maxQueued":5000,"liveState":true}"#
);
}
}