statsig_client/
error.rs

1use thiserror::Error;
2
3pub type Result<T> = std::result::Result<T, StatsigError>;
4
5#[derive(Error, Debug, Clone)]
6pub enum StatsigError {
7    #[error("API error: {status} - {message}")]
8    Api { status: u16, message: String },
9
10    #[error("Network error: {0}")]
11    Network(String),
12
13    #[error("Serialization error: {0}")]
14    Serialization(String),
15
16    #[error("Validation error: {0}")]
17    Validation(String),
18
19    #[error("Invalid configuration: {0}")]
20    Configuration(String),
21
22    #[error("Cache error: {0}")]
23    Cache(String),
24
25    #[error("Batch processor error: {0}")]
26    BatchProcessor(String),
27
28    #[error("Rate limited: retry after {retry_after_seconds} seconds")]
29    RateLimited { retry_after_seconds: u64 },
30
31    #[error("Unauthorized: invalid API key")]
32    Unauthorized,
33
34    #[error("User validation error: {0}")]
35    UserValidation(String),
36
37    #[error("Feature gate not found: {0}")]
38    GateNotFound(String),
39
40    #[error("Dynamic config not found: {0}")]
41    ConfigNotFound(String),
42
43    #[error("Internal error: {0}")]
44    Internal(String),
45}
46
47impl StatsigError {
48    pub fn api(status: u16, message: impl Into<String>) -> Self {
49        Self::Api {
50            status,
51            message: message.into(),
52        }
53    }
54
55    pub fn validation(message: impl Into<String>) -> Self {
56        Self::Validation(message.into())
57    }
58
59    pub fn configuration(message: impl Into<String>) -> Self {
60        Self::Configuration(message.into())
61    }
62
63    pub fn cache(message: impl Into<String>) -> Self {
64        Self::Cache(message.into())
65    }
66
67    pub fn batch_processor(message: impl Into<String>) -> Self {
68        Self::BatchProcessor(message.into())
69    }
70
71    pub fn rate_limited(retry_after_seconds: u64) -> Self {
72        Self::RateLimited {
73            retry_after_seconds,
74        }
75    }
76
77    pub fn user_validation(message: impl Into<String>) -> Self {
78        Self::UserValidation(message.into())
79    }
80
81    pub fn gate_not_found(name: impl Into<String>) -> Self {
82        Self::GateNotFound(name.into())
83    }
84
85    pub fn config_not_found(name: impl Into<String>) -> Self {
86        Self::ConfigNotFound(name.into())
87    }
88
89    pub fn internal(message: impl Into<String>) -> Self {
90        Self::Internal(message.into())
91    }
92
93    pub fn network(message: impl Into<String>) -> Self {
94        Self::Network(message.into())
95    }
96
97    pub fn serialization(message: impl Into<String>) -> Self {
98        Self::Serialization(message.into())
99    }
100
101    /// Adds context to an error for better debugging and error reporting
102    pub fn with_context(self, context: &str) -> Self {
103        match self {
104            Self::Api { status, message } => Self::Api {
105                status,
106                message: format!("{}: {}", context, message),
107            },
108            Self::Network(message) => Self::Network(format!("{}: {}", context, message)),
109            Self::Serialization(message) => {
110                Self::Serialization(format!("{}: {}", context, message))
111            }
112            Self::Validation(message) => Self::Validation(format!("{}: {}", context, message)),
113            Self::Configuration(message) => {
114                Self::Configuration(format!("{}: {}", context, message))
115            }
116            Self::Cache(message) => Self::Cache(format!("{}: {}", context, message)),
117            Self::BatchProcessor(message) => {
118                Self::BatchProcessor(format!("{}: {}", context, message))
119            }
120            Self::UserValidation(message) => {
121                Self::UserValidation(format!("{}: {}", context, message))
122            }
123            Self::GateNotFound(name) => Self::GateNotFound(format!("{}: {}", context, name)),
124            Self::ConfigNotFound(name) => Self::ConfigNotFound(format!("{}: {}", context, name)),
125            Self::Internal(message) => Self::Internal(format!("{}: {}", context, message)),
126            // These variants don't need context
127            Self::RateLimited { .. } | Self::Unauthorized => self,
128        }
129    }
130}
131
132impl From<reqwest::Error> for StatsigError {
133    fn from(err: reqwest::Error) -> Self {
134        Self::Network(err.to_string())
135    }
136}
137
138impl From<reqwest_middleware::Error> for StatsigError {
139    fn from(err: reqwest_middleware::Error) -> Self {
140        Self::Network(err.to_string())
141    }
142}
143
144impl StatsigError {
145    pub fn is_retryable(&self) -> bool {
146        match self {
147            Self::Network(_) | Self::RateLimited { .. } => true,
148            Self::Api { status, .. } => matches!(status, 429 | 500..=599),
149            _ => false,
150        }
151    }
152
153    pub fn retry_after_seconds(&self) -> Option<u64> {
154        match self {
155            Self::RateLimited {
156                retry_after_seconds,
157            } => Some(*retry_after_seconds),
158            Self::Api { status, .. } if *status == 429 => Some(60),
159            _ => None,
160        }
161    }
162}