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::ClientOptions;
53
54pub struct Client {
56 options: ClientOptions,
57 client: HttpClient,
58 local_evaluator: Option<LocalEvaluator>,
59 _flag_poller: Option<AsyncFlagPoller>,
60 flag_event_host: OnceLock<Arc<dyn FeatureFlagEvaluationsHost>>,
61 transport: Option<Arc<TransportHandle>>,
63}
64
65struct AsyncFlagEventHost {
69 options: ClientOptions,
70 transport: Option<Arc<TransportHandle>>,
71 dedup_cache: FlagEventDedupCache,
72}
73
74impl AsyncFlagEventHost {
75 fn from_options(options: &ClientOptions, transport: Option<Arc<TransportHandle>>) -> Self {
76 Self {
77 options: options.clone(),
78 transport,
79 dedup_cache: flag_event_dedup_cache(),
80 }
81 }
82
83 fn enqueue(&self, event: Event) {
84 if let Some(transport) = &self.transport {
85 transport.enqueue(event);
86 }
87 }
88}
89
90impl FeatureFlagEvaluationsHost for AsyncFlagEventHost {
91 fn capture_flag_called_event_if_needed(&self, params: FlagCalledEventParams) {
92 let dedup_key = build_dedup_key(¶ms.key, params.response.as_ref(), ¶ms.groups);
93 if already_reported(&self.dedup_cache, ¶ms.distinct_id, &dedup_key) {
94 return;
95 }
96
97 if let Some(event) =
98 flag_called_event(params, self.options.disable_geoip, self.options.is_server)
99 {
100 self.enqueue(event);
101 }
102 }
103
104 fn log_warning(&self, message: &str) {
105 warn!("{message}");
108 }
109}
110
111pub async fn client<C: Into<ClientOptions>>(options: C) -> Client {
127 let options = options.into().sanitize();
128 let client = HttpClient::builder()
129 .timeout(Duration::from_secs(options.request_timeout_seconds))
130 .build()
131 .unwrap(); let (local_evaluator, flag_poller) = if options.enable_local_evaluation
134 && !options.is_disabled()
135 {
136 if let Some(ref personal_key) = options.personal_api_key {
137 let cache = FlagCache::new();
138
139 let config = LocalEvaluationConfig {
140 personal_api_key: personal_key.clone(),
141 project_api_key: options.api_key.clone(),
142 api_host: options.endpoints().api_host(),
143 poll_interval: Duration::from_secs(options.poll_interval_seconds),
144 request_timeout: Duration::from_secs(options.request_timeout_seconds),
145 };
146
147 let mut poller = AsyncFlagPoller::new(config, cache.clone());
148 poller.set_on_error(options.on_error.clone());
149 poller.start().await;
150
151 (Some(LocalEvaluator::new(cache)), Some(poller))
152 } else {
153 warn!("Local evaluation enabled but personal_api_key not set, falling back to API evaluation");
154 (None, None)
155 }
156 } else {
157 (None, None)
158 };
159
160 let transport = if options.is_disabled() {
161 None
162 } else {
163 Some(Arc::new(TransportHandle::spawn(options.clone())))
164 };
165
166 Client {
167 options,
168 client,
169 local_evaluator,
170 _flag_poller: flag_poller,
171 flag_event_host: OnceLock::new(),
172 transport,
173 }
174}
175
176impl Client {
177 #[instrument(skip(self, event), level = "debug")]
192 pub fn capture(&self, event: Event) {
193 if let Some(transport) = &self.transport {
194 transport.enqueue(event);
195 }
196 }
197
198 pub async fn flush(&self) {
202 let Some(transport) = &self.transport else {
203 return;
204 };
205 if transport.is_closed() {
206 return;
207 }
208 let (tx, rx) = tokio::sync::oneshot::channel();
209 if transport.send_control(Control::Flush(Completion::Async(tx))) {
210 let _ = rx.await;
211 }
212 }
213
214 #[cfg(feature = "error-tracking")]
217 pub(crate) fn is_disabled(&self) -> bool {
218 self.options.is_disabled()
219 }
220
221 #[cfg(feature = "error-tracking")]
224 pub(crate) fn error_tracking_options(&self) -> &crate::error_tracking::ErrorTrackingOptions {
225 self.options.error_tracking()
226 }
227
228 #[cfg(test)]
232 pub(crate) fn flush_blocking(&self) {
233 if let Some(transport) = &self.transport {
234 transport.flush_blocking();
235 }
236 }
237
238 #[cfg(feature = "error-tracking")]
242 pub(crate) fn flush_blocking_timeout(&self, timeout: Duration) {
243 if let Some(transport) = &self.transport {
244 transport.flush_blocking_timeout(timeout);
245 }
246 }
247
248 #[cfg(feature = "error-tracking")]
251 pub(crate) fn on_transport_worker(&self) -> bool {
252 self.transport
253 .as_ref()
254 .is_some_and(|t| t.on_worker_thread())
255 }
256
257 #[cfg(feature = "error-tracking")]
262 pub(crate) fn enqueue_panic_event(&self, event: Event) {
263 if let Some(transport) = &self.transport {
264 transport.enqueue_panic(event);
265 }
266 }
267
268 pub async fn shutdown(&self) {
272 let Some(transport) = &self.transport else {
273 return;
274 };
275 if transport.begin_close() {
276 let (tx, rx) = tokio::sync::oneshot::channel();
277 if transport.send_control(Control::Shutdown(Completion::Async(tx))) {
278 let _ = rx.await;
279 }
280 }
281 transport.join();
286 }
287
288 #[cfg(feature = "error-tracking")]
314 pub async fn capture_exception<E>(&self, error: &E) -> Result<(), Error>
315 where
316 E: StdError + ?Sized,
317 {
318 self.capture_exception_with(error, CaptureExceptionOptions::default())
319 .await
320 }
321
322 #[cfg(feature = "error-tracking")]
349 pub async fn capture_exception_with<E>(
350 &self,
351 error: &E,
352 options: CaptureExceptionOptions,
353 ) -> Result<(), Error>
354 where
355 E: StdError + ?Sized,
356 {
357 if self.options.is_disabled() {
358 trace!("Client is disabled, skipping exception capture");
359 return Ok(());
360 }
361
362 self.capture(build_exception_event(
363 error,
364 options,
365 self.options.error_tracking(),
366 )?);
367 Ok(())
368 }
369
370 pub fn capture_batch(&self, events: Vec<Event>, historical_migration: bool) {
386 if let Some(transport) = &self.transport {
387 if historical_migration {
388 transport.enqueue_historical(events);
389 } else {
390 for event in events {
391 transport.enqueue(event);
392 }
393 }
394 }
395 }
396
397 #[cfg(feature = "test-harness")]
404 pub fn pending_events(&self) -> usize {
405 self.transport.as_ref().map_or(0, |t| t.pending())
406 }
407
408 #[must_use = "feature flags result should be used"]
434 pub async fn get_feature_flags<S: Into<String>>(
435 &self,
436 distinct_id: S,
437 groups: Option<HashMap<String, String>>,
438 person_properties: Option<HashMap<String, serde_json::Value>>,
439 group_properties: Option<HashMap<String, HashMap<String, serde_json::Value>>>,
440 ) -> Result<
441 (
442 HashMap<String, FlagValue>,
443 HashMap<String, serde_json::Value>,
444 ),
445 Error,
446 > {
447 if self.options.is_disabled() {
448 trace!("Client is disabled, skipping feature flags request");
449 return Ok((HashMap::new(), HashMap::new()));
450 }
451
452 let flags_endpoint = self.options.endpoints().build_url(Endpoint::Flags);
453
454 let mut payload = json!({
455 "api_key": self.options.api_key,
456 "distinct_id": distinct_id.into(),
457 });
458
459 if let Some(groups) = groups {
460 payload["groups"] = json!(groups);
461 }
462
463 if let Some(person_properties) = person_properties {
464 payload["person_properties"] = json!(person_properties);
465 }
466
467 if let Some(group_properties) = group_properties {
468 payload["group_properties"] = json!(group_properties);
469 }
470
471 if self.options.disable_geoip {
473 payload["disable_geoip"] = json!(true);
474 }
475
476 let response = self
477 .send_feature_flags_request(&flags_endpoint, &payload)
478 .await?;
479
480 let distinct_id = payload.get("distinct_id").and_then(|v| v.as_str());
481 if !response.status().is_success() {
482 let status = response.status();
483 let text = response
484 .text()
485 .await
486 .unwrap_or_else(|_| "Unknown error".to_string());
487 let err = Error::Connection(format!("API request failed with status {status}: {text}"));
488 report_flags_error(
489 &self.options.on_error,
490 &flags_endpoint,
491 distinct_id,
492 Some(status.as_u16()),
493 Some(&text),
494 &err,
495 );
496 return Err(err);
497 }
498
499 let status = response.status().as_u16();
500 let flags_response = match response.json::<FeatureFlagsResponse>().await {
501 Ok(r) => r,
502 Err(e) => {
503 let err =
504 Error::Serialization(format!("Failed to parse feature flags response: {e}"));
505 report_flags_error(
506 &self.options.on_error,
507 &flags_endpoint,
508 distinct_id,
509 Some(status),
510 None,
511 &err,
512 );
513 return Err(err);
514 }
515 };
516
517 Ok(flags_response.normalize())
518 }
519
520 #[must_use = "feature flag result should be used"]
541 #[instrument(skip_all, level = "debug")]
542 #[deprecated(
543 since = "0.6.0",
544 note = "Use Client::evaluate_flags() to fetch a snapshot, then call .get_flag(key) on it. \
545 The snapshot deduplicates $feature_flag_called events and supports attaching \
546 rich metadata to captured events via Event::with_flags()."
547 )]
548 pub async fn get_feature_flag<K: Into<String>, D: Into<String>>(
549 &self,
550 key: K,
551 distinct_id: D,
552 groups: Option<HashMap<String, String>>,
553 person_properties: Option<HashMap<String, serde_json::Value>>,
554 group_properties: Option<HashMap<String, HashMap<String, serde_json::Value>>>,
555 ) -> Result<Option<FlagValue>, Error> {
556 let key_str = key.into();
557 let distinct_id_str = distinct_id.into();
558
559 if let Some(ref evaluator) = self.local_evaluator {
561 let empty_props = HashMap::new();
562 let empty_groups: HashMap<String, String> = HashMap::new();
563 let empty_group_props: HashMap<String, HashMap<String, serde_json::Value>> =
564 HashMap::new();
565 let mut local_props;
566 let props = if let Some(props) = person_properties.as_ref() {
567 local_props = props.clone();
568 local_props
569 .entry("distinct_id".to_string())
570 .or_insert_with(|| json!(distinct_id_str.clone()));
571 &local_props
572 } else {
573 local_props = empty_props;
574 local_props.insert("distinct_id".to_string(), json!(distinct_id_str.clone()));
575 &local_props
576 };
577 let groups_ref = groups.as_ref().unwrap_or(&empty_groups);
578 let group_props_ref = group_properties.as_ref().unwrap_or(&empty_group_props);
579 match evaluator.evaluate_flag(
580 &key_str,
581 &distinct_id_str,
582 props,
583 groups_ref,
584 group_props_ref,
585 ) {
586 Ok(Some(value)) => {
587 debug!(flag = %key_str, ?value, "Flag evaluated locally");
588 return Ok(Some(value));
589 }
590 Ok(None) => {
591 if self.options.local_evaluation_only {
592 debug!(flag = %key_str, "Flag not found locally, skipping remote fallback");
593 return Ok(None);
594 }
595 debug!(flag = %key_str, "Flag not found locally, falling back to API");
596 }
597 Err(e) => {
598 if self.options.local_evaluation_only {
599 debug!(flag = %key_str, error = %e.message, "Inconclusive local evaluation, skipping remote fallback");
600 return Ok(None);
601 }
602 debug!(flag = %key_str, error = %e.message, "Inconclusive local evaluation, falling back to API");
603 }
604 }
605 }
606
607 trace!(flag = %key_str, "Fetching flag from API");
609 let (feature_flags, _payloads) = self
610 .get_feature_flags(distinct_id_str, groups, person_properties, group_properties)
611 .await?;
612 Ok(feature_flags.get(&key_str).cloned())
613 }
614
615 #[must_use = "feature flag enabled check result should be used"]
626 #[deprecated(
627 since = "0.6.0",
628 note = "Use Client::evaluate_flags() to fetch a snapshot, then call .is_enabled(key) \
629 on it. The snapshot deduplicates $feature_flag_called events and supports \
630 attaching rich metadata to captured events via Event::with_flags()."
631 )]
632 #[allow(deprecated)] pub async fn is_feature_enabled<K: Into<String>, D: Into<String>>(
634 &self,
635 key: K,
636 distinct_id: D,
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<bool, Error> {
641 let flag_value = self
642 .get_feature_flag(
643 key.into(),
644 distinct_id.into(),
645 groups,
646 person_properties,
647 group_properties,
648 )
649 .await?;
650 Ok(match flag_value {
651 Some(FlagValue::Boolean(b)) => b,
652 Some(FlagValue::String(_)) => true, None => false,
654 })
655 }
656
657 #[must_use = "feature flag payload result should be used"]
674 #[deprecated(
675 since = "0.6.0",
676 note = "Use Client::evaluate_flags() to fetch a snapshot, then call \
677 .get_flag_payload(key) on it. Reading the payload from a snapshot is \
678 event-free, matching this method's behavior, and avoids the per-call \
679 /flags request."
680 )]
681 pub async fn get_feature_flag_payload<K: Into<String>, D: Into<String>>(
682 &self,
683 key: K,
684 distinct_id: D,
685 ) -> Result<Option<serde_json::Value>, Error> {
686 if self.options.is_disabled() {
687 trace!("Client is disabled, skipping feature flag payload request");
688 return Ok(None);
689 }
690
691 let key_str = key.into();
692 let flags_endpoint = self.options.endpoints().build_url(Endpoint::Flags);
693
694 let mut payload = json!({
695 "api_key": self.options.api_key,
696 "distinct_id": distinct_id.into(),
697 });
698
699 if self.options.disable_geoip {
701 payload["disable_geoip"] = json!(true);
702 }
703
704 let distinct_id = payload.get("distinct_id").and_then(|v| v.as_str());
705 let response = match self
706 .client
707 .post(&flags_endpoint)
708 .header(CONTENT_TYPE, "application/json")
709 .header(USER_AGENT, get_default_user_agent())
710 .json(&payload)
711 .timeout(Duration::from_secs(
712 self.options.feature_flags_request_timeout_seconds,
713 ))
714 .send()
715 .await
716 {
717 Ok(r) => r,
718 Err(e) => {
719 let err = Error::Connection(e.to_string());
720 report_flags_error(
721 &self.options.on_error,
722 &flags_endpoint,
723 distinct_id,
724 None,
725 None,
726 &err,
727 );
728 return Err(err);
729 }
730 };
731
732 if !response.status().is_success() {
733 return Ok(None);
734 }
735
736 let status = response.status().as_u16();
737 let flags_response: FeatureFlagsResponse = match response.json().await {
738 Ok(r) => r,
739 Err(e) => {
740 let err = Error::Serialization(format!("Failed to parse response: {e}"));
741 report_flags_error(
742 &self.options.on_error,
743 &flags_endpoint,
744 distinct_id,
745 Some(status),
746 None,
747 &err,
748 );
749 return Err(err);
750 }
751 };
752
753 let (_flags, payloads) = flags_response.normalize();
754 Ok(payloads.get(&key_str).cloned())
755 }
756
757 #[allow(clippy::too_many_arguments)]
777 pub fn evaluate_feature_flag_locally(
778 &self,
779 flag: &FeatureFlag,
780 distinct_id: &str,
781 person_properties: &HashMap<String, serde_json::Value>,
782 groups: &HashMap<String, String>,
783 group_properties: &HashMap<String, HashMap<String, serde_json::Value>>,
784 ) -> Result<FlagValue, Error> {
785 let group_type_mapping = self
786 .local_evaluator
787 .as_ref()
788 .map(|ev| ev.cache().get_group_type_mapping())
789 .unwrap_or_default();
790 match_feature_flag(
791 flag,
792 distinct_id,
793 person_properties,
794 groups,
795 group_properties,
796 &group_type_mapping,
797 )
798 .map_err(|e| Error::InconclusiveMatch(e.message))
799 }
800
801 pub async fn evaluate_flags<S: Into<String>>(
824 &self,
825 distinct_id: S,
826 options: EvaluateFlagsOptions,
827 ) -> Result<FeatureFlagEvaluations, Error> {
828 let distinct_id: String = distinct_id.into();
829 let host = self.flag_event_host();
830
831 if distinct_id.is_empty() || self.options.is_disabled() {
832 return Ok(FeatureFlagEvaluations::empty(host));
833 }
834
835 let mut options = options;
836 options.groups.get_or_insert_with(HashMap::new);
837 options.group_properties.get_or_insert_with(HashMap::new);
838
839 let mut records: HashMap<String, EvaluatedFlagRecord> = HashMap::new();
840 let mut locally_evaluated_keys: HashSet<String> = HashSet::new();
841
842 if let Some(evaluator) = &self.local_evaluator {
843 let mut person_props_owned = options.person_properties.clone().unwrap_or_default();
844 person_props_owned
845 .entry("distinct_id".to_string())
846 .or_insert_with(|| json!(distinct_id.clone()));
847 let groups_owned = options.groups.clone().unwrap_or_default();
848 let group_props_owned = options.group_properties.clone().unwrap_or_default();
849 let local_results = evaluator.evaluate_all_flags(
850 &distinct_id,
851 &person_props_owned,
852 &groups_owned,
853 &group_props_owned,
854 );
855 for (key, result) in local_results {
856 if let Some(filter) = &options.flag_keys {
857 if !filter.iter().any(|k| k == &key) {
858 continue;
859 }
860 }
861 if let Ok(value) = result {
862 records.insert(key.clone(), local_record(value));
863 locally_evaluated_keys.insert(key);
864 }
865 }
866 }
867
868 let mut request_id: Option<String> = None;
869 let mut errors_while_computing = false;
870 let mut quota_limited = false;
871
872 let local_covers_request = options
877 .flag_keys
878 .as_ref()
879 .is_some_and(|keys| keys.iter().all(|k| locally_evaluated_keys.contains(k)));
880
881 if !options.only_evaluate_locally && !local_covers_request {
882 match self.fetch_flag_details(&distinct_id, &options).await {
887 Ok(response) => {
888 request_id = response.request_id;
889 errors_while_computing = response.errors_while_computing_flags;
890 quota_limited = response.quota_limited;
891 for (key, detail) in response.flags {
892 if locally_evaluated_keys.contains(&key) {
893 continue;
894 }
895 records.insert(key, remote_record_from_detail(detail));
896 }
897 }
898 Err(e) => {
899 if records.is_empty() {
900 return Err(e);
901 }
902 debug!(
903 error = e.to_string(),
904 local_count = records.len(),
905 "/flags fetch failed; returning snapshot from local results only"
906 );
907 errors_while_computing = true;
908 }
909 }
910 }
911
912 Ok(FeatureFlagEvaluations::new(
913 host,
914 distinct_id,
915 records,
916 options.groups.unwrap_or_default(),
917 options.disable_geoip,
918 request_id,
919 None,
920 errors_while_computing,
921 quota_limited,
922 ))
923 }
924
925 fn flag_event_host(&self) -> Arc<dyn FeatureFlagEvaluationsHost> {
926 self.flag_event_host
927 .get_or_init(|| {
928 Arc::new(AsyncFlagEventHost::from_options(
929 &self.options,
930 self.transport.clone(),
931 )) as Arc<dyn FeatureFlagEvaluationsHost>
932 })
933 .clone()
934 }
935
936 async fn send_feature_flags_request(
937 &self,
938 flags_endpoint: &str,
939 payload: &serde_json::Value,
940 ) -> Result<reqwest::Response, Error> {
941 let mut attempt = 1;
942 loop {
943 let request = self
944 .client
945 .post(flags_endpoint)
946 .header(CONTENT_TYPE, "application/json")
947 .header(USER_AGENT, get_default_user_agent())
948 .json(payload)
949 .timeout(Duration::from_secs(
950 self.options.feature_flags_request_timeout_seconds,
951 ));
952 #[cfg(feature = "test-harness")]
953 let request = {
954 let mut request = request;
955 if let Some(ref extra) = self.options.extra_capture_headers {
956 for (k, v) in extra {
957 request = request.header(k.as_str(), v.as_str());
958 }
959 }
960 request
961 };
962 let result = request.send().await;
963
964 match result {
965 Ok(response) => match super::retry::feature_flags_after_response(
966 &self.options,
967 attempt,
968 response.status().as_u16(),
969 ) {
970 super::retry::FeatureFlagsResponseStep::Backoff(delay) => {
971 tokio::time::sleep(delay).await;
972 attempt += 1;
973 }
974 super::retry::FeatureFlagsResponseStep::Done => return Ok(response),
975 },
976 Err(e) => {
977 let err_msg = e.to_string();
978 match super::retry::feature_flags_after_transport_error(
979 &self.options,
980 attempt,
981 is_retryable_feature_flags_error(&e),
982 err_msg,
983 ) {
984 super::retry::FeatureFlagsTransportStep::Backoff(delay) => {
985 tokio::time::sleep(delay).await;
986 attempt += 1;
987 }
988 super::retry::FeatureFlagsTransportStep::Fail(err) => {
989 report_flags_error(
990 &self.options.on_error,
991 flags_endpoint,
992 payload.get("distinct_id").and_then(|v| v.as_str()),
993 None,
994 None,
995 &err,
996 );
997 return Err(err);
998 }
999 }
1000 }
1001 }
1002 }
1003 }
1004
1005 async fn fetch_flag_details(
1006 &self,
1007 distinct_id: &str,
1008 options: &EvaluateFlagsOptions,
1009 ) -> Result<DetailedFlagsResponse, Error> {
1010 let flags_endpoint = self.options.endpoints().build_url(Endpoint::Flags);
1011
1012 let person_properties = options.person_properties.clone().unwrap_or_default();
1013 let groups = options.groups.clone().unwrap_or_default();
1014 let group_properties = options.group_properties.clone().unwrap_or_default();
1015 let effective_disable_geoip = options.disable_geoip.unwrap_or(self.options.disable_geoip);
1016
1017 let mut payload = json!({
1018 "api_key": self.options.api_key,
1019 "distinct_id": distinct_id,
1020 "groups": groups,
1021 "person_properties": person_properties,
1022 "group_properties": group_properties,
1023 "geoip_disable": effective_disable_geoip,
1024 });
1025 if let Some(flag_keys) = &options.flag_keys {
1026 payload["flag_keys_to_evaluate"] = json!(flag_keys);
1027 }
1028
1029 let response = self
1030 .send_feature_flags_request(&flags_endpoint, &payload)
1031 .await?;
1032
1033 if !response.status().is_success() {
1034 let status = response.status();
1035 let text = response
1036 .text()
1037 .await
1038 .unwrap_or_else(|_| "Unknown error".to_string());
1039 let err = Error::Connection(format!("API request failed with status {status}: {text}"));
1040 report_flags_error(
1041 &self.options.on_error,
1042 &flags_endpoint,
1043 Some(distinct_id),
1044 Some(status.as_u16()),
1045 Some(&text),
1046 &err,
1047 );
1048 return Err(err);
1049 }
1050
1051 let status = response.status().as_u16();
1052 let parsed = match response.json::<FeatureFlagsResponse>().await {
1053 Ok(p) => p,
1054 Err(e) => {
1055 let err =
1056 Error::Serialization(format!("Failed to parse feature flags response: {e}"));
1057 report_flags_error(
1058 &self.options.on_error,
1059 &flags_endpoint,
1060 Some(distinct_id),
1061 Some(status),
1062 None,
1063 &err,
1064 );
1065 return Err(err);
1066 }
1067 };
1068 Ok(extract_flag_details(parsed))
1069 }
1070}
1071
1072impl Drop for Client {
1073 fn drop(&mut self) {
1079 let Some(transport) = &self.transport else {
1080 return;
1081 };
1082 if transport.begin_close() {
1083 let (tx, rx) = std::sync::mpsc::channel();
1084 if transport.send_control(Control::Shutdown(Completion::Blocking(tx))) {
1085 let _ = rx.recv();
1086 }
1087 }
1088 transport.join();
1093 }
1094}