errsight 0.1.1

Rust client for ErrSight error tracking — captures panics, errors, and log/tracing events and ships them to the ErrSight API from a background thread.
Documentation
//! HTTP transport: the [`Transport`] trait and the default `ureq` backend.
//!
//! Transport is a trait so tests can capture payloads in-memory and advanced
//! users can swap in their own HTTP stack. The default [`UreqTransport`] uses a
//! blocking `ureq` agent with bundled Mozilla roots (rustls), which is why the
//! whole client runs on its own OS thread rather than an async runtime: the
//! send call blocks, but only the background worker thread, never the host app.

use std::time::Duration;

/// What happened to a send attempt. The worker uses this to decide whether to
/// drop the batch, pause, or just log.
#[derive(Debug)]
pub enum SendOutcome {
    /// 2xx / 202 — events accepted.
    Success,
    /// 429 — back off for the given duration, then retry the same events.
    RateLimited(Duration),
    /// Anything else (4xx/5xx/transport error). Carries a short reason for
    /// debug logging. The worker drops the batch — it does not retry, matching
    /// the Ruby SDK; the bounded queue is the backpressure mechanism.
    Error(String),
}

/// Sends a serialized JSON batch to the ingestion endpoint.
///
/// Implementations must be `Send + Sync` (the worker thread owns an `Arc<dyn
/// Transport>`) and must never panic — a transport panic would take down the
/// worker thread and silently stop all delivery.
pub trait Transport: Send + Sync {
    /// Send the given body (a JSON array of events). Must not panic.
    fn send(&self, body: &[u8]) -> SendOutcome;
}

/// Default transport built on a blocking `ureq` agent.
pub struct UreqTransport {
    agent: ureq::Agent,
    endpoint: String,
    api_key: String,
    user_agent: String,
}

impl UreqTransport {
    /// Build a transport for the given endpoint/key. `timeout` bounds connect
    /// and read so a hung server can't wedge the worker forever.
    pub fn new(endpoint: String, api_key: String, timeout: Duration) -> Self {
        let agent = ureq::AgentBuilder::new()
            .timeout_connect(timeout)
            .timeout_read(timeout)
            .timeout_write(timeout)
            .build();
        UreqTransport {
            agent,
            endpoint,
            api_key,
            user_agent: format!("errsight-rust/{}", crate::VERSION),
        }
    }
}

impl Transport for UreqTransport {
    fn send(&self, body: &[u8]) -> SendOutcome {
        let result = self
            .agent
            .post(&self.endpoint)
            .set("Content-Type", "application/json")
            .set("X-API-Key", &self.api_key)
            .set("User-Agent", &self.user_agent)
            .send_bytes(body);

        match result {
            Ok(resp) => {
                let code = resp.status();
                if (200..300).contains(&code) {
                    SendOutcome::Success
                } else {
                    SendOutcome::Error(format!("unexpected status {code}"))
                }
            }
            // ureq surfaces non-2xx as Status errors. 429 carries Retry-After.
            Err(ureq::Error::Status(code, resp)) => {
                if code == 429 {
                    let retry_after = resp
                        .header("Retry-After")
                        .and_then(|h| h.trim().parse::<u64>().ok())
                        .unwrap_or(60)
                        // Cap so a buggy/hostile upstream can't park sends for hours.
                        .min(600);
                    SendOutcome::RateLimited(Duration::from_secs(retry_after))
                } else {
                    let snippet = resp
                        .into_string()
                        .map(|s| s.chars().take(200).collect::<String>())
                        .unwrap_or_default();
                    SendOutcome::Error(format!("API error {code}: {snippet}"))
                }
            }
            Err(ureq::Error::Transport(t)) => SendOutcome::Error(format!("transport error: {t}")),
        }
    }
}