Skip to main content

tail_fin_common/
interceptor.rs

1use night_fury_core::BrowserSession;
2use serde_json::Value;
3
4use crate::error::TailFinError;
5
6/// Install a fetch interceptor that captures responses matching the given pattern.
7///
8/// This monkey-patches `window.fetch` to clone and store responses whose URL
9/// contains the specified pattern (e.g. "SearchTimeline"). The original response
10/// is returned untouched to the caller, so page behavior is unaffected.
11pub async fn install_interceptor(
12    session: &BrowserSession,
13    pattern: &str,
14) -> Result<(), TailFinError> {
15    let pattern_escaped = serde_json::to_string(pattern).unwrap_or_default();
16    let js = format!(
17        r#"
18        (() => {{
19            if (window.__tailfin_interceptor_active) return 'already_installed';
20
21            window.__tailfin_captured = [];
22            window.__tailfin_interceptor_active = true;
23
24            window.__tailfin_original_fetch = window.fetch;
25            const _originalFetch = window.fetch;
26            window.fetch = async function(...args) {{
27                const resp = await _originalFetch.apply(this, args);
28                try {{
29                    const url = typeof args[0] === 'string' ? args[0] : (args[0]?.url || '');
30                    if (url.includes({pattern_escaped})) {{
31                        const clone = resp.clone();
32                        const json = await clone.json();
33                        window.__tailfin_captured.push({{ url: url, data: json }});
34                    }}
35                }} catch (e) {{
36                    // Silently ignore parse errors for non-JSON responses
37                }}
38                return resp;
39            }};
40            return 'installed';
41        }})()
42        "#
43    );
44
45    session.eval(&js).await?;
46    Ok(())
47}
48
49/// Read all captured responses from the interceptor.
50pub async fn read_captured(session: &BrowserSession) -> Result<Vec<Value>, TailFinError> {
51    let result = session
52        .eval("JSON.parse(JSON.stringify(window.__tailfin_captured || []))")
53        .await?;
54
55    match result {
56        Value::Array(arr) => Ok(arr),
57        Value::Null => Ok(vec![]),
58        _ => Err(TailFinError::Parse(
59            "unexpected interceptor result type".into(),
60        )),
61    }
62}
63
64/// Clear all captured responses.
65pub async fn clear_captured(session: &BrowserSession) -> Result<(), TailFinError> {
66    session.eval("window.__tailfin_captured = []").await?;
67    Ok(())
68}
69
70/// Remove the fetch interceptor and restore original fetch.
71pub async fn remove_interceptor(session: &BrowserSession) -> Result<(), TailFinError> {
72    let js = r#"
73        (() => {
74            if (window.__tailfin_interceptor_active) {
75                window.fetch = window.__tailfin_original_fetch || window.fetch;
76                window.__tailfin_interceptor_active = false;
77                window.__tailfin_captured = [];
78                delete window.__tailfin_original_fetch;
79            }
80            return 'removed';
81        })()
82    "#;
83
84    session.eval(js).await?;
85    Ok(())
86}