playwright_rs_trace/
trace.rs1use crate::action::{Action, ActionStream};
5use crate::error::{Result, TraceError};
6use crate::event::{ContextOptions, RawEvent, TraceEvent};
7use crate::jsonl::JsonLines;
8use crate::network::NetworkEntry;
9use std::io::{BufRead, BufReader, Read, Seek};
10use std::path::Path;
11use zip::ZipArchive;
12
13const TRACE_ENTRY: &str = "trace.trace";
14const NETWORK_ENTRY: &str = "trace.network";
15const SUPPORTED_VERSION: u32 = 8;
16const RESOURCE_SNAPSHOT_KIND: &str = "resource-snapshot";
17
18pub struct TraceReader<R: Read + Seek> {
28 zip: ZipArchive<R>,
29 context: ContextOptions,
30}
31
32impl<R: Read + Seek> TraceReader<R> {
33 pub fn open(reader: R) -> Result<Self> {
36 let mut zip = ZipArchive::new(reader)?;
37 let context = parse_context(&mut zip)?;
38 if context.version != SUPPORTED_VERSION {
39 return Err(TraceError::UnsupportedVersion {
40 found: context.version,
41 expected: SUPPORTED_VERSION,
42 });
43 }
44 Ok(Self { zip, context })
45 }
46
47 pub fn context(&self) -> &ContextOptions {
49 &self.context
50 }
51
52 pub fn raw_events(&mut self) -> Result<impl Iterator<Item = Result<RawEvent>>> {
61 let entry = self.zip.by_name(TRACE_ENTRY)?;
62 let lines = JsonLines::new(BufReader::new(entry));
63 Ok(lines.map(|res| res.map(RawEvent::new)))
64 }
65
66 pub fn events(&mut self) -> Result<impl Iterator<Item = Result<TraceEvent>>> {
71 Ok(self
72 .raw_events()?
73 .map(|res| res.map(|raw| raw.into_typed())))
74 }
75
76 pub fn actions(&mut self) -> Result<impl Iterator<Item = Result<Action>>> {
89 Ok(ActionStream::new(self.events()?))
90 }
91
92 pub fn network(&mut self) -> Result<impl Iterator<Item = Result<NetworkEntry>>> {
99 let entry = self.zip.by_name(NETWORK_ENTRY)?;
100 let lines = JsonLines::new(BufReader::new(entry));
101 Ok(lines.map(|res| {
102 let mut map = res?;
103 let kind = map
107 .get("type")
108 .and_then(|v| v.as_str())
109 .unwrap_or("")
110 .to_string();
111 if kind != RESOURCE_SNAPSHOT_KIND {
112 return Err(TraceError::MalformedAction {
113 call_id: String::new(),
114 reason: format!(
115 "trace.network: expected `{RESOURCE_SNAPSHOT_KIND}` event, got `{kind}`",
116 ),
117 });
118 }
119 let snapshot = map
120 .remove("snapshot")
121 .ok_or_else(|| TraceError::MalformedAction {
122 call_id: String::new(),
123 reason: "trace.network: resource-snapshot missing `snapshot` payload".into(),
124 })?;
125 NetworkEntry::from_snapshot(snapshot)
126 .map_err(|source| TraceError::Json { line: 0, source })
127 }))
128 }
129}
130
131fn parse_context<R: Read + Seek>(zip: &mut ZipArchive<R>) -> Result<ContextOptions> {
132 let entry = zip
133 .by_name(TRACE_ENTRY)
134 .map_err(|_| TraceError::MissingEntry(TRACE_ENTRY))?;
135 let mut reader = BufReader::new(entry);
136 let mut line = String::new();
137 let mut line_no = 0;
138
139 loop {
140 line.clear();
141 line_no += 1;
142 let n = reader.read_line(&mut line)?;
143 if n == 0 {
144 return Err(TraceError::MissingEntry(TRACE_ENTRY));
145 }
146 let trimmed = line.trim_end_matches(['\n', '\r']);
147 if trimmed.trim().is_empty() {
148 continue;
149 }
150
151 let value: serde_json::Value =
152 serde_json::from_str(trimmed).map_err(|source| TraceError::Json {
153 line: line_no,
154 source,
155 })?;
156
157 let kind = value.get("type").and_then(|v| v.as_str()).unwrap_or("");
158 if kind != "context-options" {
159 return Err(TraceError::MalformedAction {
160 call_id: String::new(),
161 reason: format!("expected first event to be `context-options`, got `{kind}`"),
162 });
163 }
164
165 return serde_json::from_value::<ContextOptions>(value).map_err(|source| {
166 TraceError::Json {
167 line: line_no,
168 source,
169 }
170 });
171 }
172}
173
174pub fn open<P: AsRef<Path>>(path: P) -> Result<TraceReader<std::fs::File>> {
176 let file = std::fs::File::open(path)?;
177 TraceReader::open(file)
178}