1use std::collections::{HashMap, HashSet};
2#[cfg(feature = "error-tracking")]
3use std::error::Error as StdError;
4use std::sync::{Arc, OnceLock};
5use std::time::Duration;
6
7use reqwest::{header::CONTENT_TYPE, header::USER_AGENT, Client as HttpClient};
8use serde_json::json;
9use tracing::{debug, instrument, trace, warn};
10
11use super::get_default_user_agent;
12use crate::endpoints::Endpoint;
13#[cfg(feature = "error-tracking")]
14use crate::error_tracking::{build_exception_event, CaptureExceptionOptions};
15use crate::feature_flag_evaluations::{
16 EvaluateFlagsOptions, EvaluatedFlagRecord, FeatureFlagEvaluations, FeatureFlagEvaluationsHost,
17 FlagCalledEventParams,
18};
19use crate::feature_flags::{match_feature_flag, FeatureFlag, FeatureFlagsResponse, FlagValue};
20use crate::local_evaluation::{AsyncFlagPoller, FlagCache, LocalEvaluationConfig, LocalEvaluator};
21use crate::{Error, Event};
22
23fn is_retryable_feature_flags_error(err: &reqwest::Error) -> bool {
24 if err.is_timeout() {
25 return true;
26 }
27
28 let mut source = std::error::Error::source(err);
29 while let Some(error) = source {
30 if let Some(io_error) = error.downcast_ref::<std::io::Error>() {
31 return matches!(
32 io_error.kind(),
33 std::io::ErrorKind::ConnectionReset
34 | std::io::ErrorKind::TimedOut
35 | std::io::ErrorKind::UnexpectedEof
36 );
37 }
38 source = std::error::Error::source(error);
39 }
40
41 !err.to_string()
42 .to_lowercase()
43 .contains("connection refused")
44}
45
46use super::common::{
47 already_reported, build_dedup_key, extract_flag_details, flag_called_event,
48 flag_event_dedup_cache, local_record, remote_record_from_detail, report_flags_error,
49 DetailedFlagsResponse, FlagEventDedupCache,
50};
51use super::transport::{Completion, Control, TransportHandle};
52use super::{CaptureSummary, ClientOptions};
53#[cfg(not(feature = "capture-v1"))]
54use reqwest::header::CONTENT_ENCODING;
55
56pub struct Client {
58 options: ClientOptions,
59 client: HttpClient,
60 local_evaluator: Option<LocalEvaluator>,
61 _flag_poller: Option<AsyncFlagPoller>,
62 flag_event_host: OnceLock<Arc<dyn FeatureFlagEvaluationsHost>>,
63 transport: Option<Arc<TransportHandle>>,
65}
66
67struct AsyncFlagEventHost {
71 options: ClientOptions,
72 transport: Option<Arc<TransportHandle>>,
73 dedup_cache: FlagEventDedupCache,
74}
75
76impl AsyncFlagEventHost {
77 fn from_options(options: &ClientOptions, transport: Option<Arc<TransportHandle>>) -> Self {
78 Self {
79 options: options.clone(),
80 transport,
81 dedup_cache: flag_event_dedup_cache(),
82 }
83 }
84
85 fn enqueue(&self, event: Event) {
86 if let Some(transport) = &self.transport {
87 transport.enqueue(event);
88 }
89 }
90}
91
92impl FeatureFlagEvaluationsHost for AsyncFlagEventHost {
93 fn capture_flag_called_event_if_needed(&self, params: FlagCalledEventParams) {
94 let dedup_key = build_dedup_key(¶ms.key, params.response.as_ref(), ¶ms.groups);
95 if already_reported(&self.dedup_cache, ¶ms.distinct_id, &dedup_key) {
96 return;
97 }
98
99 if let Some(event) =
100 flag_called_event(params, self.options.disable_geoip, self.options.is_server)
101 {
102 self.enqueue(event);
103 }
104 }
105
106 fn log_warning(&self, message: &str) {
107 warn!("{message}");
110 }
111}
112
113pub async fn client<C: Into<ClientOptions>>(options: C) -> Client {
129 let options = options.into().sanitize();
130 let client = HttpClient::builder()
131 .timeout(Duration::from_secs(options.request_timeout_seconds))
132 .build()
133 .unwrap(); let (local_evaluator, flag_poller) =
136 if options.enable_local_evaluation && !options.is_disabled() {
137 if let Some(ref secret_key) = options.secret_key {
138 let cache = FlagCache::new();
139
140 let config = LocalEvaluationConfig {
141 personal_api_key: secret_key.clone(),
142 project_api_key: options.api_key.clone(),
143 api_host: options.endpoints().api_host(),
144 poll_interval: Duration::from_secs(options.poll_interval_seconds),
145 request_timeout: Duration::from_secs(options.request_timeout_seconds),
146 };
147
148 let mut poller = AsyncFlagPoller::new(config, cache.clone());
149 poller.set_on_error(options.on_error.clone());
150 poller.start().await;
151
152 (Some(LocalEvaluator::new(cache)), Some(poller))
153 } else {
154 warn!(
155 "Local evaluation enabled but secret_key not set, falling back to API evaluation"
156 );
157 (None, None)
158 }
159 } else {
160 (None, None)
161 };
162
163 let transport = if options.is_disabled() {
164 None
165 } else {
166 Some(Arc::new(TransportHandle::spawn(options.clone())))
167 };
168
169 Client {
170 options,
171 client,
172 local_evaluator,
173 _flag_poller: flag_poller,
174 flag_event_host: OnceLock::new(),
175 transport,
176 }
177}
178
179impl Client {
180 #[instrument(skip(self, event), level = "debug")]
195 pub fn capture(&self, event: Event) {
196 if let Some(transport) = &self.transport {
197 transport.enqueue(event);
198 }
199 }
200
201 pub async fn flush(&self) {
205 let Some(transport) = &self.transport else {
206 return;
207 };
208 if transport.is_closed() {
209 return;
210 }
211 let (tx, rx) = tokio::sync::oneshot::channel();
212 if transport.send_control(Control::Flush(Completion::Async(tx))) {
213 let _ = rx.await;
214 }
215 }
216
217 #[cfg(feature = "error-tracking")]
220 pub(crate) fn is_disabled(&self) -> bool {
221 self.options.is_disabled()
222 }
223
224 #[cfg(feature = "error-tracking")]
227 pub(crate) fn error_tracking_options(&self) -> &crate::error_tracking::ErrorTrackingOptions {
228 self.options.error_tracking()
229 }
230
231 #[cfg(test)]
235 pub(crate) fn flush_blocking(&self) {
236 if let Some(transport) = &self.transport {
237 transport.flush_blocking();
238 }
239 }
240
241 #[cfg(feature = "error-tracking")]
245 pub(crate) fn flush_blocking_timeout(&self, timeout: Duration) {
246 if let Some(transport) = &self.transport {
247 transport.flush_blocking_timeout(timeout);
248 }
249 }
250
251 #[cfg(feature = "error-tracking")]
254 pub(crate) fn on_transport_worker(&self) -> bool {
255 self.transport
256 .as_ref()
257 .is_some_and(|t| t.on_worker_thread())
258 }
259
260 #[cfg(feature = "error-tracking")]
265 pub(crate) fn enqueue_panic_event(&self, event: Event) {
266 if let Some(transport) = &self.transport {
267 transport.enqueue_panic(event);
268 }
269 }
270
271 pub async fn shutdown(&self) {
275 let Some(transport) = &self.transport else {
276 return;
277 };
278 if transport.begin_close() {
279 let (tx, rx) = tokio::sync::oneshot::channel();
280 if transport.send_control(Control::Shutdown(Completion::Async(tx))) {
281 let _ = rx.await;
282 }
283 }
284 transport.join();
289 }
290
291 #[cfg(feature = "error-tracking")]
317 pub async fn capture_exception<E>(&self, error: &E) -> Result<(), Error>
318 where
319 E: StdError + ?Sized,
320 {
321 self.capture_exception_with(error, CaptureExceptionOptions::default())
322 .await
323 }
324
325 #[cfg(feature = "error-tracking")]
352 pub async fn capture_exception_with<E>(
353 &self,
354 error: &E,
355 options: CaptureExceptionOptions,
356 ) -> Result<(), Error>
357 where
358 E: StdError + ?Sized,
359 {
360 if self.options.is_disabled() {
361 trace!("Client is disabled, skipping exception capture");
362 return Ok(());
363 }
364
365 self.capture(build_exception_event(
366 error,
367 options,
368 self.options.error_tracking(),
369 )?);
370 Ok(())
371 }
372
373 pub fn capture_batch(&self, events: Vec<Event>, historical_migration: bool) {
389 if let Some(transport) = &self.transport {
390 if historical_migration {
391 transport.enqueue_historical(events);
392 } else {
393 for event in events {
394 transport.enqueue(event);
395 }
396 }
397 }
398 }
399
400 #[must_use = "the delivery outcome should be inspected"]
415 pub async fn capture_immediate(&self, event: Event) -> Result<CaptureSummary, Error> {
416 self.capture_batch_immediate(vec![event], false).await
417 }
418
419 #[must_use = "the delivery outcome should be inspected"]
446 #[instrument(
447 skip(self, events),
448 fields(event_count = events.len(), historical_migration),
449 level = "debug"
450 )]
451 pub async fn capture_batch_immediate(
452 &self,
453 events: Vec<Event>,
454 historical_migration: bool,
455 ) -> Result<CaptureSummary, Error> {
456 if self.options.is_disabled() || events.is_empty() {
457 return Ok(CaptureSummary::default());
458 }
459 self.send_immediate(events, historical_migration).await
460 }
461
462 #[cfg(feature = "capture-v1")]
466 async fn send_immediate(
467 &self,
468 events: Vec<Event>,
469 historical_migration: bool,
470 ) -> Result<CaptureSummary, Error> {
471 use super::v1_capture::{self, Step};
472
473 let Some(mut prep) =
474 v1_capture::prepare_immediate(&self.options, events, historical_migration)
475 else {
476 return Ok(CaptureSummary::default());
477 };
478 let mut final_results = HashMap::new();
479 let mut attempt: u32 = 1;
480
481 loop {
482 let (headers, body) = v1_capture::build_attempt_parts(
483 &self.options,
484 &prep.request_id,
485 attempt,
486 &prep.created_at,
487 prep.historical_migration,
488 &prep.pending,
489 )?;
490
491 let step = match self
492 .client
493 .post(&prep.url)
494 .headers(headers)
495 .body(body)
496 .send()
497 .await
498 {
499 Err(e) => v1_capture::after_transport_error(
500 &self.options,
501 &prep.request_id,
502 attempt,
503 e.to_string(),
504 ),
505 Ok(response) => {
506 let status = response.status().as_u16();
507 let retry_after = v1_capture::parse_retry_after(response.headers());
508 let text = response
509 .text()
510 .await
511 .unwrap_or_else(|_| "Unknown error".to_string());
512 v1_capture::after_response(
513 &self.options,
514 &prep.request_id,
515 attempt,
516 status,
517 retry_after,
518 &text,
519 &mut prep.pending,
520 &mut final_results,
521 )
522 }
523 };
524
525 match step {
526 Step::Done => {
527 return Ok(CaptureSummary::from_results(prep.submitted, final_results))
528 }
529 Step::Fail(e) => return Err(e),
530 Step::Backoff(delay) => {
531 attempt += 1;
532 tokio::time::sleep(delay).await;
533 }
534 }
535 }
536 }
537
538 #[cfg(not(feature = "capture-v1"))]
541 async fn send_immediate(
542 &self,
543 events: Vec<Event>,
544 historical_migration: bool,
545 ) -> Result<CaptureSummary, Error> {
546 use super::retry::{self, v0_after_response, v0_after_transport_error, Step};
547 use super::v0_capture;
548
549 let Some(prep) =
550 v0_capture::prepare_immediate(&self.options, events, historical_migration)?
551 else {
552 return Ok(CaptureSummary::default());
553 };
554
555 let mut attempt: u32 = 1;
556 loop {
557 let mut request = self
558 .client
559 .post(&prep.url)
560 .header(CONTENT_TYPE, "application/json")
561 .header(USER_AGENT, get_default_user_agent())
562 .body(prep.body.clone());
563 if let Some(token) = prep.encoding {
564 request = request.header(CONTENT_ENCODING, token);
565 }
566 #[cfg(feature = "test-harness")]
567 if let Some(ref extra) = self.options.extra_capture_headers {
568 for (k, v) in extra {
569 request = request.header(k.as_str(), v.as_str());
570 }
571 }
572
573 let step = match request.send().await {
574 Err(e) => v0_after_transport_error(&self.options, attempt, e.to_string()),
575 Ok(response) => {
576 let status = response.status().as_u16();
577 let retry_after = retry::parse_retry_after(response.headers());
578 let text = response
579 .text()
580 .await
581 .unwrap_or_else(|_| "Unknown error".to_string());
582 v0_after_response(&self.options, attempt, status, retry_after, &text)
583 }
584 };
585
586 match step {
587 Step::Done => return Ok(CaptureSummary::delivered(prep.kept)),
588 Step::Fail(e) => return Err(e),
589 Step::Backoff(delay) => {
590 attempt += 1;
591 tokio::time::sleep(delay).await;
592 }
593 }
594 }
595 }
596
597 #[cfg(feature = "test-harness")]
604 pub fn pending_events(&self) -> usize {
605 self.transport.as_ref().map_or(0, |t| t.pending())
606 }
607
608 #[must_use = "feature flags result should be used"]
634 pub async fn get_feature_flags<S: Into<String>>(
635 &self,
636 distinct_id: S,
637 groups: Option<HashMap<String, String>>,
638 person_properties: Option<HashMap<String, serde_json::Value>>,
639 group_properties: Option<HashMap<String, HashMap<String, serde_json::Value>>>,
640 ) -> Result<
641 (
642 HashMap<String, FlagValue>,
643 HashMap<String, serde_json::Value>,
644 ),
645 Error,
646 > {
647 if self.options.is_disabled() {
648 trace!("Client is disabled, skipping feature flags request");
649 return Ok((HashMap::new(), HashMap::new()));
650 }
651
652 let flags_endpoint = self.options.endpoints().build_url(Endpoint::Flags);
653
654 let mut payload = json!({
655 "api_key": self.options.api_key,
656 "distinct_id": distinct_id.into(),
657 });
658
659 if let Some(groups) = groups {
660 payload["groups"] = json!(groups);
661 }
662
663 if let Some(person_properties) = person_properties {
664 payload["person_properties"] = json!(person_properties);
665 }
666
667 if let Some(group_properties) = group_properties {
668 payload["group_properties"] = json!(group_properties);
669 }
670
671 if self.options.disable_geoip {
673 payload["disable_geoip"] = json!(true);
674 }
675
676 let response = self
677 .send_feature_flags_request(&flags_endpoint, &payload)
678 .await?;
679
680 let distinct_id = payload.get("distinct_id").and_then(|v| v.as_str());
681 if !response.status().is_success() {
682 let status = response.status();
683 let text = response
684 .text()
685 .await
686 .unwrap_or_else(|_| "Unknown error".to_string());
687 let err = Error::Connection(format!("API request failed with status {status}: {text}"));
688 report_flags_error(
689 &self.options.on_error,
690 &flags_endpoint,
691 distinct_id,
692 Some(status.as_u16()),
693 Some(&text),
694 &err,
695 );
696 return Err(err);
697 }
698
699 let status = response.status().as_u16();
700 let flags_response = match response.json::<FeatureFlagsResponse>().await {
701 Ok(r) => r,
702 Err(e) => {
703 let err =
704 Error::Serialization(format!("Failed to parse feature flags response: {e}"));
705 report_flags_error(
706 &self.options.on_error,
707 &flags_endpoint,
708 distinct_id,
709 Some(status),
710 None,
711 &err,
712 );
713 return Err(err);
714 }
715 };
716
717 Ok(flags_response.normalize())
718 }
719
720 #[must_use = "feature flag result should be used"]
741 #[instrument(skip_all, level = "debug")]
742 #[deprecated(
743 since = "0.6.0",
744 note = "Use Client::evaluate_flags() to fetch a snapshot, then call .get_flag(key) on it. \
745 The snapshot deduplicates $feature_flag_called events and supports attaching \
746 rich metadata to captured events via Event::with_flags()."
747 )]
748 pub async fn get_feature_flag<K: Into<String>, D: Into<String>>(
749 &self,
750 key: K,
751 distinct_id: D,
752 groups: Option<HashMap<String, String>>,
753 person_properties: Option<HashMap<String, serde_json::Value>>,
754 group_properties: Option<HashMap<String, HashMap<String, serde_json::Value>>>,
755 ) -> Result<Option<FlagValue>, Error> {
756 let key_str = key.into();
757 let distinct_id_str = distinct_id.into();
758
759 if let Some(ref evaluator) = self.local_evaluator {
761 let empty_props = HashMap::new();
762 let empty_groups: HashMap<String, String> = HashMap::new();
763 let empty_group_props: HashMap<String, HashMap<String, serde_json::Value>> =
764 HashMap::new();
765 let mut local_props;
766 let props = if let Some(props) = person_properties.as_ref() {
767 local_props = props.clone();
768 local_props
769 .entry("distinct_id".to_string())
770 .or_insert_with(|| json!(distinct_id_str.clone()));
771 &local_props
772 } else {
773 local_props = empty_props;
774 local_props.insert("distinct_id".to_string(), json!(distinct_id_str.clone()));
775 &local_props
776 };
777 let groups_ref = groups.as_ref().unwrap_or(&empty_groups);
778 let group_props_ref = group_properties.as_ref().unwrap_or(&empty_group_props);
779 match evaluator.evaluate_flag(
780 &key_str,
781 &distinct_id_str,
782 props,
783 groups_ref,
784 group_props_ref,
785 ) {
786 Ok(Some(value)) => {
787 debug!(flag = %key_str, ?value, "Flag evaluated locally");
788 return Ok(Some(value));
789 }
790 Ok(None) => {
791 if self.options.local_evaluation_only {
792 debug!(flag = %key_str, "Flag not found locally, skipping remote fallback");
793 return Ok(None);
794 }
795 debug!(flag = %key_str, "Flag not found locally, falling back to API");
796 }
797 Err(e) => {
798 if self.options.local_evaluation_only {
799 debug!(flag = %key_str, error = %e.message, "Inconclusive local evaluation, skipping remote fallback");
800 return Ok(None);
801 }
802 debug!(flag = %key_str, error = %e.message, "Inconclusive local evaluation, falling back to API");
803 }
804 }
805 }
806
807 trace!(flag = %key_str, "Fetching flag from API");
809 let (feature_flags, _payloads) = self
810 .get_feature_flags(distinct_id_str, groups, person_properties, group_properties)
811 .await?;
812 Ok(feature_flags.get(&key_str).cloned())
813 }
814
815 #[must_use = "feature flag enabled check result should be used"]
826 #[deprecated(
827 since = "0.6.0",
828 note = "Use Client::evaluate_flags() to fetch a snapshot, then call .is_enabled(key) \
829 on it. The snapshot deduplicates $feature_flag_called events and supports \
830 attaching rich metadata to captured events via Event::with_flags()."
831 )]
832 #[allow(deprecated)] pub async fn is_feature_enabled<K: Into<String>, D: Into<String>>(
834 &self,
835 key: K,
836 distinct_id: D,
837 groups: Option<HashMap<String, String>>,
838 person_properties: Option<HashMap<String, serde_json::Value>>,
839 group_properties: Option<HashMap<String, HashMap<String, serde_json::Value>>>,
840 ) -> Result<bool, Error> {
841 let flag_value = self
842 .get_feature_flag(
843 key.into(),
844 distinct_id.into(),
845 groups,
846 person_properties,
847 group_properties,
848 )
849 .await?;
850 Ok(match flag_value {
851 Some(FlagValue::Boolean(b)) => b,
852 Some(FlagValue::String(_)) => true, None => false,
854 })
855 }
856
857 #[must_use = "feature flag payload result should be used"]
874 #[deprecated(
875 since = "0.6.0",
876 note = "Use Client::evaluate_flags() to fetch a snapshot, then call \
877 .get_flag_payload(key) on it. Reading the payload from a snapshot is \
878 event-free, matching this method's behavior, and avoids the per-call \
879 /flags request."
880 )]
881 pub async fn get_feature_flag_payload<K: Into<String>, D: Into<String>>(
882 &self,
883 key: K,
884 distinct_id: D,
885 ) -> Result<Option<serde_json::Value>, Error> {
886 if self.options.is_disabled() {
887 trace!("Client is disabled, skipping feature flag payload request");
888 return Ok(None);
889 }
890
891 let key_str = key.into();
892 let flags_endpoint = self.options.endpoints().build_url(Endpoint::Flags);
893
894 let mut payload = json!({
895 "api_key": self.options.api_key,
896 "distinct_id": distinct_id.into(),
897 });
898
899 if self.options.disable_geoip {
901 payload["disable_geoip"] = json!(true);
902 }
903
904 let distinct_id = payload.get("distinct_id").and_then(|v| v.as_str());
905 let response = match self
906 .client
907 .post(&flags_endpoint)
908 .header(CONTENT_TYPE, "application/json")
909 .header(USER_AGENT, get_default_user_agent())
910 .json(&payload)
911 .timeout(Duration::from_secs(
912 self.options.feature_flags_request_timeout_seconds,
913 ))
914 .send()
915 .await
916 {
917 Ok(r) => r,
918 Err(e) => {
919 let err = Error::Connection(e.to_string());
920 report_flags_error(
921 &self.options.on_error,
922 &flags_endpoint,
923 distinct_id,
924 None,
925 None,
926 &err,
927 );
928 return Err(err);
929 }
930 };
931
932 if !response.status().is_success() {
933 return Ok(None);
934 }
935
936 let status = response.status().as_u16();
937 let flags_response: FeatureFlagsResponse = match response.json().await {
938 Ok(r) => r,
939 Err(e) => {
940 let err = Error::Serialization(format!("Failed to parse response: {e}"));
941 report_flags_error(
942 &self.options.on_error,
943 &flags_endpoint,
944 distinct_id,
945 Some(status),
946 None,
947 &err,
948 );
949 return Err(err);
950 }
951 };
952
953 let (_flags, payloads) = flags_response.normalize();
954 Ok(payloads.get(&key_str).cloned())
955 }
956
957 #[allow(clippy::too_many_arguments)]
977 pub fn evaluate_feature_flag_locally(
978 &self,
979 flag: &FeatureFlag,
980 distinct_id: &str,
981 person_properties: &HashMap<String, serde_json::Value>,
982 groups: &HashMap<String, String>,
983 group_properties: &HashMap<String, HashMap<String, serde_json::Value>>,
984 ) -> Result<FlagValue, Error> {
985 let group_type_mapping = self
986 .local_evaluator
987 .as_ref()
988 .map(|ev| ev.cache().get_group_type_mapping())
989 .unwrap_or_default();
990 match_feature_flag(
991 flag,
992 distinct_id,
993 person_properties,
994 groups,
995 group_properties,
996 &group_type_mapping,
997 )
998 .map_err(|e| Error::InconclusiveMatch(e.message))
999 }
1000
1001 pub async fn evaluate_flags<S: Into<String>>(
1024 &self,
1025 distinct_id: S,
1026 options: EvaluateFlagsOptions,
1027 ) -> Result<FeatureFlagEvaluations, Error> {
1028 let distinct_id: String = distinct_id.into();
1029 let host = self.flag_event_host();
1030
1031 if distinct_id.is_empty() || self.options.is_disabled() {
1032 return Ok(FeatureFlagEvaluations::empty(host));
1033 }
1034
1035 let mut options = options;
1036 options.groups.get_or_insert_with(HashMap::new);
1037 options.group_properties.get_or_insert_with(HashMap::new);
1038
1039 let mut records: HashMap<String, EvaluatedFlagRecord> = HashMap::new();
1040 let mut locally_evaluated_keys: HashSet<String> = HashSet::new();
1041
1042 if let Some(evaluator) = &self.local_evaluator {
1043 let mut person_props_owned = options.person_properties.clone().unwrap_or_default();
1044 person_props_owned
1045 .entry("distinct_id".to_string())
1046 .or_insert_with(|| json!(distinct_id.clone()));
1047 let groups_owned = options.groups.clone().unwrap_or_default();
1048 let group_props_owned = options.group_properties.clone().unwrap_or_default();
1049 let local_results = evaluator.evaluate_all_flags(
1050 &distinct_id,
1051 &person_props_owned,
1052 &groups_owned,
1053 &group_props_owned,
1054 );
1055 for (key, result) in local_results {
1056 if let Some(filter) = &options.flag_keys {
1057 if !filter.iter().any(|k| k == &key) {
1058 continue;
1059 }
1060 }
1061 if let Ok(value) = result {
1062 records.insert(key.clone(), local_record(value));
1063 locally_evaluated_keys.insert(key);
1064 }
1065 }
1066 }
1067
1068 let mut request_id: Option<String> = None;
1069 let mut errors_while_computing = false;
1070 let mut quota_limited = false;
1071
1072 let local_covers_request = options
1077 .flag_keys
1078 .as_ref()
1079 .is_some_and(|keys| keys.iter().all(|k| locally_evaluated_keys.contains(k)));
1080
1081 if !options.only_evaluate_locally && !local_covers_request {
1082 match self.fetch_flag_details(&distinct_id, &options).await {
1087 Ok(response) => {
1088 request_id = response.request_id;
1089 errors_while_computing = response.errors_while_computing_flags;
1090 quota_limited = response.quota_limited;
1091 for (key, detail) in response.flags {
1092 if locally_evaluated_keys.contains(&key) {
1093 continue;
1094 }
1095 records.insert(key, remote_record_from_detail(detail));
1096 }
1097 }
1098 Err(e) => {
1099 if records.is_empty() {
1100 return Err(e);
1101 }
1102 debug!(
1103 error = e.to_string(),
1104 local_count = records.len(),
1105 "/flags fetch failed; returning snapshot from local results only"
1106 );
1107 errors_while_computing = true;
1108 }
1109 }
1110 }
1111
1112 Ok(FeatureFlagEvaluations::new(
1113 host,
1114 distinct_id,
1115 records,
1116 options.groups.unwrap_or_default(),
1117 options.disable_geoip,
1118 request_id,
1119 None,
1120 errors_while_computing,
1121 quota_limited,
1122 ))
1123 }
1124
1125 fn flag_event_host(&self) -> Arc<dyn FeatureFlagEvaluationsHost> {
1126 self.flag_event_host
1127 .get_or_init(|| {
1128 Arc::new(AsyncFlagEventHost::from_options(
1129 &self.options,
1130 self.transport.clone(),
1131 )) as Arc<dyn FeatureFlagEvaluationsHost>
1132 })
1133 .clone()
1134 }
1135
1136 async fn send_feature_flags_request(
1137 &self,
1138 flags_endpoint: &str,
1139 payload: &serde_json::Value,
1140 ) -> Result<reqwest::Response, Error> {
1141 let mut attempt = 1;
1142 loop {
1143 let request = self
1144 .client
1145 .post(flags_endpoint)
1146 .header(CONTENT_TYPE, "application/json")
1147 .header(USER_AGENT, get_default_user_agent())
1148 .json(payload)
1149 .timeout(Duration::from_secs(
1150 self.options.feature_flags_request_timeout_seconds,
1151 ));
1152 #[cfg(feature = "test-harness")]
1153 let request = {
1154 let mut request = request;
1155 if let Some(ref extra) = self.options.extra_capture_headers {
1156 for (k, v) in extra {
1157 request = request.header(k.as_str(), v.as_str());
1158 }
1159 }
1160 request
1161 };
1162 let result = request.send().await;
1163
1164 match result {
1165 Ok(response) => match super::retry::feature_flags_after_response(
1166 &self.options,
1167 attempt,
1168 response.status().as_u16(),
1169 ) {
1170 super::retry::FeatureFlagsResponseStep::Backoff(delay) => {
1171 tokio::time::sleep(delay).await;
1172 attempt += 1;
1173 }
1174 super::retry::FeatureFlagsResponseStep::Done => return Ok(response),
1175 },
1176 Err(e) => {
1177 let err_msg = e.to_string();
1178 match super::retry::feature_flags_after_transport_error(
1179 &self.options,
1180 attempt,
1181 is_retryable_feature_flags_error(&e),
1182 err_msg,
1183 ) {
1184 super::retry::FeatureFlagsTransportStep::Backoff(delay) => {
1185 tokio::time::sleep(delay).await;
1186 attempt += 1;
1187 }
1188 super::retry::FeatureFlagsTransportStep::Fail(err) => {
1189 report_flags_error(
1190 &self.options.on_error,
1191 flags_endpoint,
1192 payload.get("distinct_id").and_then(|v| v.as_str()),
1193 None,
1194 None,
1195 &err,
1196 );
1197 return Err(err);
1198 }
1199 }
1200 }
1201 }
1202 }
1203 }
1204
1205 async fn fetch_flag_details(
1206 &self,
1207 distinct_id: &str,
1208 options: &EvaluateFlagsOptions,
1209 ) -> Result<DetailedFlagsResponse, Error> {
1210 let flags_endpoint = self.options.endpoints().build_url(Endpoint::Flags);
1211
1212 let person_properties = options.person_properties.clone().unwrap_or_default();
1213 let groups = options.groups.clone().unwrap_or_default();
1214 let group_properties = options.group_properties.clone().unwrap_or_default();
1215 let effective_disable_geoip = options.disable_geoip.unwrap_or(self.options.disable_geoip);
1216
1217 let mut payload = json!({
1218 "api_key": self.options.api_key,
1219 "distinct_id": distinct_id,
1220 "groups": groups,
1221 "person_properties": person_properties,
1222 "group_properties": group_properties,
1223 "geoip_disable": effective_disable_geoip,
1224 });
1225 if let Some(flag_keys) = &options.flag_keys {
1226 payload["flag_keys_to_evaluate"] = json!(flag_keys);
1227 }
1228
1229 let response = self
1230 .send_feature_flags_request(&flags_endpoint, &payload)
1231 .await?;
1232
1233 if !response.status().is_success() {
1234 let status = response.status();
1235 let text = response
1236 .text()
1237 .await
1238 .unwrap_or_else(|_| "Unknown error".to_string());
1239 let err = Error::Connection(format!("API request failed with status {status}: {text}"));
1240 report_flags_error(
1241 &self.options.on_error,
1242 &flags_endpoint,
1243 Some(distinct_id),
1244 Some(status.as_u16()),
1245 Some(&text),
1246 &err,
1247 );
1248 return Err(err);
1249 }
1250
1251 let status = response.status().as_u16();
1252 let parsed = match response.json::<FeatureFlagsResponse>().await {
1253 Ok(p) => p,
1254 Err(e) => {
1255 let err =
1256 Error::Serialization(format!("Failed to parse feature flags response: {e}"));
1257 report_flags_error(
1258 &self.options.on_error,
1259 &flags_endpoint,
1260 Some(distinct_id),
1261 Some(status),
1262 None,
1263 &err,
1264 );
1265 return Err(err);
1266 }
1267 };
1268 Ok(extract_flag_details(parsed))
1269 }
1270}
1271
1272impl Drop for Client {
1273 fn drop(&mut self) {
1279 let Some(transport) = &self.transport else {
1280 return;
1281 };
1282 if transport.begin_close() {
1283 let (tx, rx) = std::sync::mpsc::channel();
1284 if transport.send_control(Control::Shutdown(Completion::Blocking(tx))) {
1285 let _ = rx.recv();
1286 }
1287 }
1288 transport.join();
1293 }
1294}