viewpoint_core/context/har/
mod.rs

1//! Context-level HAR recording.
2
3use std::path::PathBuf;
4
5use tracing::debug;
6
7use crate::error::NetworkError;
8use crate::network::{HarRecorder, HarRecordingBuilder, HarRecordingOptions};
9
10use super::BrowserContext;
11
12impl BrowserContext {
13    /// Start recording network traffic to a HAR file.
14    ///
15    /// All network requests and responses will be captured and saved to the
16    /// specified path when `close()` is called or when `save_har()` is called.
17    ///
18    /// # Example
19    ///
20    /// ```no_run
21    /// use viewpoint_core::Browser;
22    ///
23    /// # async fn example() -> Result<(), viewpoint_core::CoreError> {
24    /// let browser = Browser::launch().headless(true).launch().await?;
25    /// let mut context = browser.new_context().await?;
26    /// let page = context.new_page().await?;
27    ///
28    /// // Basic recording
29    /// context.record_har("output.har").await?;
30    ///
31    /// // Navigate and make requests...
32    /// page.goto("https://example.com").goto().await?;
33    ///
34    /// // HAR is saved automatically on context.close()
35    /// context.close().await?;
36    /// # Ok(())
37    /// # }
38    /// ```
39    ///
40    /// # Errors
41    ///
42    /// Returns an error if:
43    /// - HAR recording is already active
44    /// - The context is closed
45    pub async fn record_har(
46        &self,
47        path: impl Into<PathBuf>,
48    ) -> Result<HarRecordingBuilder, NetworkError> {
49        if self.is_closed() {
50            return Err(NetworkError::Aborted);
51        }
52
53        let recorder = self.har_recorder.read().await;
54        if recorder.is_some() {
55            return Err(NetworkError::AlreadyHandled);
56        }
57        drop(recorder);
58
59        Ok(HarRecordingBuilder::new(path))
60    }
61
62    /// Start HAR recording with the given options.
63    ///
64    /// # Example
65    ///
66    /// ```no_run
67    /// use viewpoint_core::Browser;
68    /// use viewpoint_core::network::HarRecordingBuilder;
69    ///
70    /// # async fn example() -> Result<(), viewpoint_core::CoreError> {
71    /// let browser = Browser::launch().headless(true).launch().await?;
72    /// let context = browser.new_context().await?;
73    ///
74    /// // Record only API requests
75    /// context.start_har_recording(
76    ///     HarRecordingBuilder::new("api.har")
77    ///         .url_filter("**/api/**")
78    ///         .build()
79    /// ).await?;
80    /// # Ok(())
81    /// # }
82    /// ```
83    ///
84    /// ```no_run
85    /// use viewpoint_core::Browser;
86    /// use viewpoint_core::network::HarRecordingBuilder;
87    ///
88    /// # async fn example() -> Result<(), viewpoint_core::CoreError> {
89    /// let browser = Browser::launch().headless(true).launch().await?;
90    /// let context = browser.new_context().await?;
91    ///
92    /// // Omit response content
93    /// context.start_har_recording(
94    ///     HarRecordingBuilder::new("requests.har")
95    ///         .omit_content(true)
96    ///         .build()
97    /// ).await?;
98    /// # Ok(())
99    /// # }
100    /// ```
101    ///
102    /// # Errors
103    ///
104    /// Returns an error if the context is closed or HAR recording is already active.
105    pub async fn start_har_recording(
106        &self,
107        options: HarRecordingOptions,
108    ) -> Result<(), NetworkError> {
109        if self.is_closed() {
110            return Err(NetworkError::Aborted);
111        }
112
113        let mut recorder_lock = self.har_recorder.write().await;
114        if recorder_lock.is_some() {
115            return Err(NetworkError::AlreadyHandled);
116        }
117
118        let recorder = HarRecorder::new(options)?;
119        *recorder_lock = Some(recorder);
120
121        debug!("Started HAR recording");
122        Ok(())
123    }
124
125    /// Save the current HAR recording to file.
126    ///
127    /// # Example
128    ///
129    /// ```no_run
130    /// use viewpoint_core::Browser;
131    ///
132    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
133    /// let browser = Browser::launch().headless(true).launch().await?;
134    /// let context = browser.new_context().await?;
135    ///
136    /// context.record_har("output.har").await?;
137    /// // ... do some navigation ...
138    /// let path = context.save_har().await?;
139    /// println!("HAR saved to: {}", path.display());
140    /// # Ok(())
141    /// # }
142    /// ```
143    ///
144    /// # Errors
145    ///
146    /// Returns an error if:
147    /// - No HAR recording is active
148    /// - Failed to write the file
149    pub async fn save_har(&self) -> Result<PathBuf, NetworkError> {
150        let recorder = self.har_recorder.read().await;
151        match recorder.as_ref() {
152            Some(rec) => rec.save().await,
153            None => Err(NetworkError::InvalidResponse(
154                "No HAR recording is active".to_string(),
155            )),
156        }
157    }
158
159    /// Stop HAR recording and optionally save to file.
160    ///
161    /// If `save` is true, the HAR file is saved before stopping.
162    ///
163    /// # Errors
164    ///
165    /// Returns an error if saving the HAR file fails.
166    pub async fn stop_har_recording(&self, save: bool) -> Result<Option<PathBuf>, NetworkError> {
167        let mut recorder_lock = self.har_recorder.write().await;
168        if let Some(recorder) = recorder_lock.take() {
169            if save {
170                let path = recorder.save().await?;
171                return Ok(Some(path));
172            }
173        }
174        Ok(None)
175    }
176}