tail-fin-common 0.1.0

Shared infrastructure for tail-fin: error types, page_fetch, cookies, CDP helpers
Documentation
use night_fury_core::BrowserSession;
use serde_json::Value;

use crate::error::TailFinError;

/// Install a fetch interceptor that captures responses matching the given pattern.
///
/// This monkey-patches `window.fetch` to clone and store responses whose URL
/// contains the specified pattern (e.g. "SearchTimeline"). The original response
/// is returned untouched to the caller, so page behavior is unaffected.
pub async fn install_interceptor(
    session: &BrowserSession,
    pattern: &str,
) -> Result<(), TailFinError> {
    let pattern_escaped = serde_json::to_string(pattern).unwrap_or_default();
    let js = format!(
        r#"
        (() => {{
            if (window.__tailfin_interceptor_active) return 'already_installed';

            window.__tailfin_captured = [];
            window.__tailfin_interceptor_active = true;

            window.__tailfin_original_fetch = window.fetch;
            const _originalFetch = window.fetch;
            window.fetch = async function(...args) {{
                const resp = await _originalFetch.apply(this, args);
                try {{
                    const url = typeof args[0] === 'string' ? args[0] : (args[0]?.url || '');
                    if (url.includes({pattern_escaped})) {{
                        const clone = resp.clone();
                        const json = await clone.json();
                        window.__tailfin_captured.push({{ url: url, data: json }});
                    }}
                }} catch (e) {{
                    // Silently ignore parse errors for non-JSON responses
                }}
                return resp;
            }};
            return 'installed';
        }})()
        "#
    );

    session.eval(&js).await?;
    Ok(())
}

/// Read all captured responses from the interceptor.
pub async fn read_captured(session: &BrowserSession) -> Result<Vec<Value>, TailFinError> {
    let result = session
        .eval("JSON.parse(JSON.stringify(window.__tailfin_captured || []))")
        .await?;

    match result {
        Value::Array(arr) => Ok(arr),
        Value::Null => Ok(vec![]),
        _ => Err(TailFinError::Parse(
            "unexpected interceptor result type".into(),
        )),
    }
}

/// Clear all captured responses.
pub async fn clear_captured(session: &BrowserSession) -> Result<(), TailFinError> {
    session.eval("window.__tailfin_captured = []").await?;
    Ok(())
}

/// Remove the fetch interceptor and restore original fetch.
pub async fn remove_interceptor(session: &BrowserSession) -> Result<(), TailFinError> {
    let js = r#"
        (() => {
            if (window.__tailfin_interceptor_active) {
                window.fetch = window.__tailfin_original_fetch || window.fetch;
                window.__tailfin_interceptor_active = false;
                window.__tailfin_captured = [];
                delete window.__tailfin_original_fetch;
            }
            return 'removed';
        })()
    "#;

    session.eval(js).await?;
    Ok(())
}