Skip to main content

playwright_rs/protocol/
tracing.rs

1// Copyright 2026 Paul Adamson
2// Licensed under the Apache License, Version 2.0
3//
4// Tracing — Playwright trace recording
5//
6// Architecture Reference:
7// - Python: playwright-python/playwright/_impl/_tracing.py
8// - JavaScript: playwright/packages/playwright-core/src/client/tracing.ts
9// - Docs: https://playwright.dev/docs/api/class-tracing
10
11//! Tracing — record Playwright traces for debugging
12//!
13//! Tracing is a per-context feature. Access the Tracing object via
14//! [`BrowserContext::tracing`](crate::protocol::BrowserContext::tracing).
15//!
16//! # Example
17//!
18//! ```no_run
19//! use playwright_rs::protocol::{Playwright, TracingStartOptions};
20//!
21//! #[tokio::main]
22//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
23//!     let playwright = Playwright::launch().await?;
24//!     let browser = playwright.chromium().launch().await?;
25//!     let context = browser.new_context().await?;
26//!
27//!     let tracing = context.tracing().await?;
28//!
29//!     // Start tracing with options
30//!     tracing.start(Some(TracingStartOptions::default()
31//!         .name("my-trace")
32//!         .screenshots(true)
33//!         .snapshots(true))).await?;
34//!
35//!     let page = context.new_page().await?;
36//!     page.goto("https://example.com", None).await?;
37//!
38//!     // Stop and save the trace
39//!     use playwright_rs::protocol::TracingStopOptions;
40//!     tracing.stop(Some(TracingStopOptions::default()
41//!         .path("/tmp/trace.zip"))).await?;
42//!
43//!     context.close().await?;
44//!     browser.close().await?;
45//!     Ok(())
46//! }
47//! ```
48//!
49//! See: <https://playwright.dev/docs/api/class-tracing>
50
51use crate::error::Result;
52use crate::protocol::har_options::StartHarOptions;
53use crate::server::channel::Channel;
54use crate::server::channel_owner::{
55    ChannelOwner, ChannelOwnerImpl, DisposeReason, ParentOrConnection,
56};
57use crate::server::connection::ConnectionLike;
58use serde_json::Value;
59use std::any::Any;
60use std::sync::Arc;
61
62/// Options for starting a trace recording.
63///
64/// See: <https://playwright.dev/docs/api/class-tracing#tracing-start>
65#[derive(Debug, Clone, Default)]
66#[non_exhaustive]
67pub struct TracingStartOptions {
68    /// Custom name for the trace. Shown in trace viewer as the trace title.
69    pub name: Option<String>,
70    /// Whether to capture screenshots during tracing. Screenshots are used as
71    /// a timeline preview in the trace viewer.
72    pub screenshots: Option<bool>,
73    /// Whether to capture DOM snapshots on each action.
74    pub snapshots: Option<bool>,
75    /// Whether to enable live trace updates while recording. When `true`,
76    /// the trace viewer can attach and observe the trace as it is being
77    /// captured, rather than waiting for the recording to finish. Useful
78    /// for debugging long-running flows.
79    ///
80    /// See: <https://playwright.dev/docs/api/class-tracing#tracing-start-option-live>
81    pub live: Option<bool>,
82}
83
84impl TracingStartOptions {
85    /// Trace name (affects file naming in the traces directory).
86    pub fn name(mut self, name: impl Into<String>) -> Self {
87        self.name = Some(name.into());
88        self
89    }
90    /// Capture screenshots during tracing.
91    pub fn screenshots(mut self, screenshots: bool) -> Self {
92        self.screenshots = Some(screenshots);
93        self
94    }
95    /// Capture DOM snapshots during tracing.
96    pub fn snapshots(mut self, snapshots: bool) -> Self {
97        self.snapshots = Some(snapshots);
98        self
99    }
100    /// Enable live tracing (view in the trace viewer while running).
101    pub fn live(mut self, live: bool) -> Self {
102        self.live = Some(live);
103        self
104    }
105}
106
107/// Options for stopping a trace recording.
108///
109/// See: <https://playwright.dev/docs/api/class-tracing#tracing-stop>
110#[derive(Debug, Clone, Default)]
111#[non_exhaustive]
112pub struct TracingStopOptions {
113    /// Path to export the trace file to. If not provided, the trace is discarded.
114    /// The file is written as a `.zip` archive.
115    pub path: Option<String>,
116}
117
118impl TracingStopOptions {
119    /// Export the trace to the given path.
120    pub fn path(mut self, path: impl Into<String>) -> Self {
121        self.path = Some(path.into());
122        self
123    }
124}
125
126/// In-flight HAR recording state, captured by `start_har` for `stop_har`.
127struct HarRecording {
128    har_id: Option<String>,
129    path: String,
130    resources_dir: Option<String>,
131}
132
133/// Tracing — records Playwright traces for debugging and inspection.
134///
135/// Trace files can be opened in the Playwright Trace Viewer.
136/// This is a Chromium-only feature; calling tracing methods on Firefox or
137/// WebKit contexts will fail.
138///
139/// See: <https://playwright.dev/docs/api/class-tracing>
140#[derive(Clone)]
141pub struct Tracing {
142    base: ChannelOwnerImpl,
143    /// Shared across clones so `start_har`/`stop_har` on the same context's
144    /// `Tracing` see one recording. `stop_har` takes no path (matching the
145    /// upstream API), so the path and `harId` are stashed here at start.
146    har: Arc<parking_lot::Mutex<Option<HarRecording>>>,
147}
148
149impl Tracing {
150    /// Creates a new Tracing from protocol initialization.
151    ///
152    /// Called by the object factory when the server sends a `__create__` message.
153    pub fn new(
154        parent: ParentOrConnection,
155        type_name: String,
156        guid: Arc<str>,
157        initializer: Value,
158    ) -> Result<Self> {
159        Ok(Self {
160            base: ChannelOwnerImpl::new(parent, type_name, guid, initializer),
161            har: Arc::new(parking_lot::Mutex::new(None)),
162        })
163    }
164
165    /// Start tracing.
166    ///
167    /// Playwright implements tracing as a two-step process: `tracingStart` to
168    /// configure the trace, then `tracingStartChunk` to begin recording.
169    ///
170    /// # Arguments
171    ///
172    /// * `options` - Optional trace configuration (name, screenshots, snapshots)
173    ///
174    /// # Errors
175    ///
176    /// Returns error if:
177    /// - Tracing is already active
178    /// - Communication with browser process fails
179    ///
180    /// See: <https://playwright.dev/docs/api/class-tracing#tracing-start>
181    #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
182    pub async fn start(&self, options: impl Into<Option<TracingStartOptions>>) -> Result<()> {
183        let options = options.into();
184        let opts = options.unwrap_or_default();
185
186        // Step 1: tracingStart — configure the trace
187        let mut start_params = serde_json::json!({});
188        if let Some(ref name) = opts.name {
189            start_params["name"] = serde_json::Value::String(name.clone());
190        }
191        if let Some(screenshots) = opts.screenshots {
192            start_params["screenshots"] = serde_json::Value::Bool(screenshots);
193        }
194        if let Some(snapshots) = opts.snapshots {
195            start_params["snapshots"] = serde_json::Value::Bool(snapshots);
196        }
197        if let Some(live) = opts.live {
198            start_params["live"] = serde_json::Value::Bool(live);
199        }
200
201        self.channel()
202            .send_no_result("tracingStart", start_params)
203            .await?;
204
205        // Step 2: tracingStartChunk — begin the chunk/recording
206        let mut chunk_params = serde_json::json!({});
207        if let Some(name) = opts.name {
208            chunk_params["name"] = serde_json::Value::String(name);
209        }
210
211        self.channel()
212            .send_no_result("tracingStartChunk", chunk_params)
213            .await
214    }
215
216    /// Stop tracing.
217    ///
218    /// Playwright implements stopping as a two-step process: `tracingStopChunk`
219    /// to finalize the recording, then `tracingStop` to tear down.
220    ///
221    /// If `options.path` is provided, the trace is exported to that file as a
222    /// `.zip` archive. If no path is provided, the trace is discarded.
223    ///
224    /// # Arguments
225    ///
226    /// * `options` - Optional stop options; set `path` to save the trace to a file
227    ///
228    /// # Errors
229    ///
230    /// Returns error if:
231    /// - Tracing was not active
232    /// - Communication with browser process fails
233    ///
234    /// See: <https://playwright.dev/docs/api/class-tracing#tracing-stop>
235    #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
236    pub async fn stop(&self, options: impl Into<Option<TracingStopOptions>>) -> Result<()> {
237        let options = options.into();
238        let path = options.and_then(|o| o.path);
239
240        // Step 1: tracingStopChunk — mode "entries" collects trace data
241        // mode "archive" or "compressedTrace" would export, but "entries" is simpler
242        let mode = if path.is_some() { "archive" } else { "discard" };
243        let stop_chunk_params = serde_json::json!({ "mode": mode });
244
245        let chunk_result: Value = self
246            .channel()
247            .send("tracingStopChunk", stop_chunk_params)
248            .await?;
249
250        // Step 2: tracingStop — tear down
251        self.channel()
252            .send_no_result("tracingStop", serde_json::json!({}))
253            .await?;
254
255        // If a path was requested, save the artifact
256        if let Some(dest_path) = path
257            && let Some(artifact_guid) = chunk_result
258                .get("artifact")
259                .and_then(|a| a.get("guid"))
260                .and_then(|g| g.as_str())
261        {
262            // Resolve the artifact and save it
263            self.save_artifact(artifact_guid, &dest_path).await?;
264        }
265
266        Ok(())
267    }
268
269    /// Save a trace artifact to a file path.
270    async fn save_artifact(&self, artifact_guid: &str, dest_path: &str) -> Result<()> {
271        use crate::protocol::artifact::Artifact;
272        use crate::server::connection::ConnectionExt;
273
274        let artifact = self
275            .connection()
276            .get_typed::<Artifact>(artifact_guid)
277            .await?;
278
279        artifact.save_as(dest_path).await
280    }
281
282    /// Start recording a HAR (HTTP Archive) of network traffic to `path`.
283    ///
284    /// The HAR is written when [`stop_har`](Self::stop_har) is called. A `.zip`
285    /// path bundles resource bodies as separate entries (`Attach`); a plain
286    /// path inlines them (`Embed`). The recorded HAR can be opened in browser
287    /// devtools or replayed in tests via `route_from_har`.
288    ///
289    /// # Errors
290    ///
291    /// Returns an error if communication with the browser process fails.
292    ///
293    /// See: <https://playwright.dev/docs/api/class-tracing#tracing-start-har>
294    #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
295    pub async fn start_har(
296        &self,
297        path: impl Into<String>,
298        options: impl Into<Option<StartHarOptions>>,
299    ) -> Result<()> {
300        let options = options.into();
301        let path = path.into();
302        let opts = options.unwrap_or_default();
303        let rec_options = opts.to_record_har_json(&path);
304
305        let result: Value = self
306            .channel()
307            .send("harStart", serde_json::json!({ "options": rec_options }))
308            .await?;
309        let har_id = result
310            .get("harId")
311            .and_then(|v| v.as_str())
312            .map(str::to_owned);
313
314        *self.har.lock() = Some(HarRecording {
315            har_id,
316            path,
317            resources_dir: opts.resources_dir,
318        });
319        Ok(())
320    }
321
322    /// Stop the HAR recording started by [`start_har`](Self::start_har) and
323    /// write it to the path given there.
324    ///
325    /// # Errors
326    ///
327    /// Returns an error if `start_har` was not called first, or if
328    /// communication with the browser process fails.
329    ///
330    /// See: <https://playwright.dev/docs/api/class-tracing#tracing-stop-har>
331    #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
332    pub async fn stop_har(&self) -> Result<()> {
333        let Some(recording) = self.har.lock().take() else {
334            return Err(crate::error::Error::InvalidArgument(
335                "stop_har called without a matching start_har".to_string(),
336            ));
337        };
338
339        let mut params = serde_json::json!({ "mode": "archive" });
340        if let Some(id) = &recording.har_id {
341            params["harId"] = Value::String(id.clone());
342        }
343
344        let result: Value = self.channel().send("harExport", params).await?;
345
346        let Some(artifact_guid) = result
347            .get("artifact")
348            .and_then(|a| a.get("guid"))
349            .and_then(|g| g.as_str())
350        else {
351            return Ok(());
352        };
353
354        // harExport always yields a zip archive. A `.zip` destination takes it
355        // verbatim; any other path gets the `.har` JSON extracted out of it.
356        if recording.path.ends_with(".zip") {
357            self.save_artifact(artifact_guid, &recording.path).await?;
358        } else {
359            let tmp_zip = format!("{}.tmp.zip", recording.path);
360            self.save_artifact(artifact_guid, &tmp_zip).await?;
361            let local_utils = self.find_local_utils()?;
362            local_utils
363                .har_unzip(
364                    &tmp_zip,
365                    &recording.path,
366                    recording.resources_dir.as_deref(),
367                )
368                .await?;
369            let _ = std::fs::remove_file(&tmp_zip);
370        }
371
372        Ok(())
373    }
374
375    /// Locate the connection's `LocalUtils` (used to extract a `.har` from the
376    /// exported zip archive).
377    fn find_local_utils(&self) -> Result<crate::protocol::LocalUtils> {
378        let connection = self.connection();
379        connection
380            .all_objects_sync()
381            .into_iter()
382            .find(|o| o.type_name() == "LocalUtils")
383            .and_then(|o| {
384                o.as_any()
385                    .downcast_ref::<crate::protocol::LocalUtils>()
386                    .cloned()
387            })
388            .ok_or_else(|| {
389                crate::error::Error::ProtocolError(
390                    "stop_har: LocalUtils not found in connection registry".to_string(),
391                )
392            })
393    }
394}
395
396impl ChannelOwner for Tracing {
397    fn guid(&self) -> &str {
398        self.base.guid()
399    }
400
401    fn type_name(&self) -> &str {
402        self.base.type_name()
403    }
404
405    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
406        self.base.parent()
407    }
408
409    fn connection(&self) -> Arc<dyn ConnectionLike> {
410        self.base.connection()
411    }
412
413    fn initializer(&self) -> &Value {
414        self.base.initializer()
415    }
416
417    fn channel(&self) -> &Channel {
418        self.base.channel()
419    }
420
421    fn dispose(&self, reason: DisposeReason) {
422        self.base.dispose(reason)
423    }
424
425    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
426        self.base.adopt(child)
427    }
428
429    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
430        self.base.add_child(guid, child)
431    }
432
433    fn remove_child(&self, guid: &str) {
434        self.base.remove_child(guid)
435    }
436
437    fn on_event(&self, method: &str, params: Value) {
438        self.base.on_event(method, params)
439    }
440
441    fn was_collected(&self) -> bool {
442        self.base.was_collected()
443    }
444
445    fn as_any(&self) -> &dyn Any {
446        self
447    }
448}
449
450impl std::fmt::Debug for Tracing {
451    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
452        f.debug_struct("Tracing")
453            .field("guid", &self.guid())
454            .finish()
455    }
456}