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
7#[cfg(feature = "capture-v1")]
8use chrono::Utc;
9use reqwest::header::USER_AGENT;
10use reqwest::{header::CONTENT_TYPE, Client as HttpClient};
11use serde_json::json;
12use tracing::{debug, instrument, trace, warn};
13#[cfg(feature = "capture-v1")]
14use uuid::Uuid;
15
16use super::get_default_user_agent;
17use crate::endpoints::Endpoint;
18#[cfg(feature = "error-tracking")]
19use crate::error_tracking::{build_exception_event, CaptureExceptionOptions};
20#[cfg(not(feature = "capture-v1"))]
21use crate::event::InnerEvent;
22#[cfg(feature = "capture-v1")]
23use crate::event_v1::CaptureResponse;
24use crate::feature_flag_evaluations::{
25 EvaluateFlagsOptions, EvaluatedFlagRecord, FeatureFlagEvaluations, FeatureFlagEvaluationsHost,
26 FlagCalledEventParams,
27};
28use crate::feature_flags::{match_feature_flag, FeatureFlag, FeatureFlagsResponse, FlagValue};
29use crate::local_evaluation::{AsyncFlagPoller, FlagCache, LocalEvaluationConfig, LocalEvaluator};
30use crate::{Error, Event};
31
32#[cfg(feature = "capture-v1")]
33use super::common::apply_capture_defaults;
34use super::common::{
35 already_reported, apply_before_send_hooks, build_dedup_key, extract_flag_details,
36 flag_called_event, flag_event_dedup_cache, local_record, remote_record_from_detail,
37 DetailedFlagsResponse, FlagEventDedupCache,
38};
39use super::{BeforeSendHook, ClientOptions};
40
41#[cfg(not(feature = "capture-v1"))]
42async fn check_response(response: reqwest::Response) -> Result<(), Error> {
43 let status = response.status().as_u16();
44 let body = response
45 .text()
46 .await
47 .unwrap_or_else(|_| "Unknown error".to_string());
48
49 match Error::from_http_response(status, body) {
50 Some(err) => Err(err),
51 None => Ok(()),
52 }
53}
54
55pub struct Client {
57 options: ClientOptions,
58 client: HttpClient,
59 local_evaluator: Option<LocalEvaluator>,
60 _flag_poller: Option<AsyncFlagPoller>,
61 flag_event_host: OnceLock<Arc<dyn FeatureFlagEvaluationsHost>>,
62}
63
64struct AsyncFlagEventHost {
72 http_client: HttpClient,
73 options: ClientOptions,
74 capture_url: String,
75 #[cfg_attr(feature = "capture-v1", allow(dead_code))]
78 before_send: Vec<BeforeSendHook>,
79 dedup_cache: FlagEventDedupCache,
80 runtime: tokio::runtime::Handle,
86}
87
88impl AsyncFlagEventHost {
89 fn from_options(options: &ClientOptions, http_client: HttpClient) -> Self {
90 #[cfg(feature = "capture-v1")]
91 let capture_url = options
92 .endpoints()
93 .build_custom_url(super::v1_capture::V1_CAPTURE_PATH);
94 #[cfg(not(feature = "capture-v1"))]
95 let capture_url = options.endpoints().build_url(Endpoint::Capture);
96 Self {
97 http_client,
98 options: options.clone(),
99 capture_url,
100 before_send: options.before_send.clone(),
101 dedup_cache: flag_event_dedup_cache(),
102 runtime: tokio::runtime::Handle::current(),
103 }
104 }
105
106 fn spawn_ship(&self, event: Event) {
107 if self.options.is_disabled() {
108 return;
109 }
110 #[cfg(feature = "capture-v1")]
111 self.spawn_ship_v1(event);
112 #[cfg(not(feature = "capture-v1"))]
113 self.spawn_ship_v0(event);
114 }
115
116 #[cfg(feature = "capture-v1")]
119 fn spawn_ship_v1(&self, event: Event) {
120 let (headers, body) =
121 match super::v1_capture::build_flag_event_request(&self.options, &event) {
122 Ok(parts) => parts,
123 Err(e) => {
124 debug!(error = %e, "failed to serialize $feature_flag_called event");
125 return;
126 }
127 };
128 let http_client = self.http_client.clone();
129 let url = self.capture_url.clone();
130 self.runtime.spawn(async move {
131 match http_client
132 .post(&url)
133 .headers(headers)
134 .body(body)
135 .send()
136 .await
137 {
138 Ok(resp) => {
139 let status = resp.status().as_u16();
140 if !(200..=299).contains(&status) {
141 let message = resp
142 .text()
143 .await
144 .unwrap_or_else(|_| "Unknown error".to_string());
145 debug!(
146 status,
147 "$feature_flag_called event rejected by server: {message}"
148 );
149 }
150 }
151 Err(send_err) => {
152 let message = send_err.to_string();
153 debug!("failed to send $feature_flag_called event: {message}");
154 }
155 }
156 });
157 }
158
159 #[cfg(not(feature = "capture-v1"))]
160 fn spawn_ship_v0(&self, mut event: Event) {
161 event.prepare_for_v0();
162 let Some(event) = apply_before_send_hooks(&self.before_send, event) else {
163 return;
164 };
165 let inner_event = InnerEvent::new(event, self.options.api_key.clone());
166 let payload = match serde_json::to_string(&inner_event) {
167 Ok(p) => p,
168 Err(e) => {
169 debug!(error = %e, "failed to serialize $feature_flag_called event");
170 return;
171 }
172 };
173 let http_client = self.http_client.clone();
174 let url = self.capture_url.clone();
175 self.runtime.spawn(async move {
176 let response = match http_client
177 .post(&url)
178 .header(CONTENT_TYPE, "application/json")
179 .header(USER_AGENT, get_default_user_agent())
180 .body(payload)
181 .send()
182 .await
183 {
184 Ok(r) => r,
185 Err(send_err) => {
186 let message = send_err.to_string();
187 debug!("failed to send $feature_flag_called event: {message}");
188 return;
189 }
190 };
191 if let Err(check_err) = check_response(response).await {
192 let message = check_err.to_string();
193 debug!("$feature_flag_called event rejected by server: {message}");
194 }
195 });
196 }
197}
198
199impl FeatureFlagEvaluationsHost for AsyncFlagEventHost {
200 fn capture_flag_called_event_if_needed(&self, params: FlagCalledEventParams) {
201 let dedup_key = build_dedup_key(¶ms.key, params.response.as_ref(), ¶ms.groups);
202 if already_reported(&self.dedup_cache, ¶ms.distinct_id, &dedup_key) {
203 return;
204 }
205
206 if let Some(event) =
207 flag_called_event(params, self.options.disable_geoip, self.options.is_server)
208 {
209 self.spawn_ship(event);
210 }
211 }
212
213 fn log_warning(&self, message: &str) {
214 warn!("{message}");
217 }
218}
219
220pub async fn client<C: Into<ClientOptions>>(options: C) -> Client {
236 let options = options.into().sanitize();
237 let client = HttpClient::builder()
238 .timeout(Duration::from_secs(options.request_timeout_seconds))
239 .build()
240 .unwrap(); let (local_evaluator, flag_poller) = if options.enable_local_evaluation
243 && !options.is_disabled()
244 {
245 if let Some(ref personal_key) = options.personal_api_key {
246 let cache = FlagCache::new();
247
248 let config = LocalEvaluationConfig {
249 personal_api_key: personal_key.clone(),
250 project_api_key: options.api_key.clone(),
251 api_host: options.endpoints().api_host(),
252 poll_interval: Duration::from_secs(options.poll_interval_seconds),
253 request_timeout: Duration::from_secs(options.request_timeout_seconds),
254 };
255
256 let mut poller = AsyncFlagPoller::new(config, cache.clone());
257 poller.start().await;
258
259 (Some(LocalEvaluator::new(cache)), Some(poller))
260 } else {
261 warn!("Local evaluation enabled but personal_api_key not set, falling back to API evaluation");
262 (None, None)
263 }
264 } else {
265 (None, None)
266 };
267
268 Client {
269 options,
270 client,
271 local_evaluator,
272 _flag_poller: flag_poller,
273 flag_event_host: OnceLock::new(),
274 }
275}
276
277impl Client {
278 #[instrument(skip(self, event), level = "debug")]
296 pub async fn capture(&self, event: Event) -> Result<(), Error> {
297 if self.options.is_disabled() {
298 trace!("Client is disabled, skipping capture");
299 return Ok(());
300 }
301
302 #[cfg(feature = "capture-v1")]
303 {
304 let mut event = event;
305 let defaults = self.options.capture_defaults();
306 apply_capture_defaults(&mut event, &defaults);
307 let Some(event) = apply_before_send_hooks(&self.options.before_send, event) else {
308 return Ok(());
309 };
310 return self.capture_v1(vec![event], false).await.map(|_| ());
311 }
312
313 #[cfg(not(feature = "capture-v1"))]
314 self.capture_v0(event).await
315 }
316
317 #[cfg(feature = "error-tracking")]
343 pub async fn capture_exception<E>(&self, error: &E) -> Result<(), Error>
344 where
345 E: StdError + ?Sized,
346 {
347 self.capture_exception_with(error, CaptureExceptionOptions::default())
348 .await
349 }
350
351 #[cfg(feature = "error-tracking")]
378 pub async fn capture_exception_with<E>(
379 &self,
380 error: &E,
381 options: CaptureExceptionOptions,
382 ) -> Result<(), Error>
383 where
384 E: StdError + ?Sized,
385 {
386 if self.options.is_disabled() {
387 trace!("Client is disabled, skipping exception capture");
388 return Ok(());
389 }
390
391 self.capture(build_exception_event(
392 error,
393 options,
394 self.options.error_tracking(),
395 )?)
396 .await
397 }
398
399 pub async fn capture_batch(
413 &self,
414 events: Vec<Event>,
415 historical_migration: bool,
416 ) -> Result<(), Error> {
417 if self.options.is_disabled() {
418 return Ok(());
419 }
420 if events.is_empty() {
421 return Ok(());
422 }
423
424 #[cfg(feature = "capture-v1")]
425 {
426 let defaults = self.options.capture_defaults();
427 let events: Vec<_> = events
428 .into_iter()
429 .filter_map(|mut event| {
430 apply_capture_defaults(&mut event, &defaults);
431 apply_before_send_hooks(&self.options.before_send, event)
432 })
433 .collect();
434 if events.is_empty() {
435 return Ok(());
436 }
437 return self
438 .capture_v1(events, historical_migration)
439 .await
440 .map(|_| ());
441 }
442
443 #[cfg(not(feature = "capture-v1"))]
444 self.capture_batch_v0(events, historical_migration).await
445 }
446
447 #[cfg(not(feature = "capture-v1"))]
448 async fn capture_v0(&self, mut event: Event) -> Result<(), Error> {
449 let defaults = self.options.capture_defaults();
450 super::v0_capture::prepare_event(&mut event, &defaults);
451 let Some(event) = apply_before_send_hooks(&self.options.before_send, event) else {
452 return Ok(());
453 };
454 let payload =
455 super::v0_capture::build_capture_payload(event, self.options.api_key.clone())?;
456 let url = self.options.endpoints().build_url(Endpoint::Capture);
457 let (body, encoding) = super::v0_capture::encode_body(&self.options, payload);
458 self.send_v0_with_retry(&url, body, encoding).await
459 }
460
461 #[cfg(not(feature = "capture-v1"))]
462 async fn capture_batch_v0(
463 &self,
464 events: Vec<Event>,
465 historical_migration: bool,
466 ) -> Result<(), Error> {
467 let defaults = self.options.capture_defaults();
468 let Some(payload) = super::v0_capture::build_batch_payload(
469 events,
470 self.options.api_key.clone(),
471 historical_migration,
472 &defaults,
473 &self.options.before_send,
474 )?
475 else {
476 return Ok(());
477 };
478 let url = self.options.endpoints().build_url(Endpoint::Batch);
479 let (body, encoding) = super::v0_capture::encode_body(&self.options, payload);
480 self.send_v0_with_retry(&url, body, encoding).await
481 }
482
483 #[cfg(not(feature = "capture-v1"))]
492 async fn send_v0_with_retry(
493 &self,
494 url: &str,
495 body: Vec<u8>,
496 encoding: Option<&'static str>,
497 ) -> Result<(), Error> {
498 use super::retry::{v0_after_response, v0_after_transport_error, Step};
499
500 let url = match encoding {
503 Some(token) => format!("{url}?compression={token}"),
504 None => url.to_string(),
505 };
506 let mut attempt: u32 = 1;
507 loop {
508 let mut request = self
509 .client
510 .post(&url)
511 .header(CONTENT_TYPE, "application/json")
512 .header(USER_AGENT, get_default_user_agent())
513 .body(body.clone());
514 if let Some(token) = encoding {
515 request = request.header(reqwest::header::CONTENT_ENCODING, token);
516 }
517 let request = super::v0_capture::apply_extra_headers(&self.options, request);
518
519 let step = match request.send().await {
520 Err(e) => v0_after_transport_error(&self.options, attempt, e.to_string()),
521 Ok(response) => {
522 let status = response.status().as_u16();
523 let retry_after = super::retry::parse_retry_after(response.headers());
524 let body = response
525 .text()
526 .await
527 .unwrap_or_else(|_| "Unknown error".to_string());
528 v0_after_response(&self.options, attempt, status, retry_after, &body)
529 }
530 };
531
532 match step {
533 Step::Done => return Ok(()),
534 Step::Fail(e) => return Err(e),
535 Step::Backoff(delay) => {
536 tokio::time::sleep(delay).await;
537 attempt += 1;
538 }
539 }
540 }
541 }
542
543 #[cfg(feature = "capture-v1")]
544 async fn capture_v1(
545 &self,
546 events: Vec<Event>,
547 historical_migration: bool,
548 ) -> Result<CaptureResponse, Error> {
549 use super::v1_capture::{self, Step};
550 use crate::event_v1::V1BatchRequestRef;
551
552 let request_id = Uuid::now_v7();
553 let created_at = Utc::now().to_rfc3339();
554 let mut attempt: u32 = 1;
555 let defaults = self.options.capture_defaults();
556 let mut pending = v1_capture::build_events(&events, &defaults);
557 let mut final_results = HashMap::new();
558 let historical_migration = historical_migration.then_some(true);
559 let url = self
560 .options
561 .endpoints()
562 .build_custom_url(v1_capture::V1_CAPTURE_PATH);
563
564 loop {
565 let req = V1BatchRequestRef {
566 created_at: &created_at,
567 historical_migration,
568 batch: &pending,
569 };
570 let payload =
571 serde_json::to_vec(&req).map_err(|e| Error::Serialization(e.to_string()))?;
572 let mut headers = v1_capture::build_headers(&self.options, &request_id, attempt);
573 let body =
574 v1_capture::maybe_compress(self.options.capture_compression, &mut headers, payload);
575
576 let step = match self
577 .client
578 .post(&url)
579 .headers(headers)
580 .body(body)
581 .send()
582 .await
583 {
584 Err(e) => v1_capture::after_transport_error(
585 &self.options,
586 &request_id,
587 attempt,
588 e.to_string(),
589 ),
590 Ok(resp) => {
591 let status = resp.status().as_u16();
592 let retry_after = v1_capture::parse_retry_after(resp.headers());
593 let text = resp
594 .text()
595 .await
596 .unwrap_or_else(|_| "Unknown error".to_string());
597 v1_capture::after_response(
598 &self.options,
599 &request_id,
600 attempt,
601 status,
602 retry_after,
603 &text,
604 &mut pending,
605 &mut final_results,
606 )
607 }
608 };
609
610 match step {
611 Step::Done => break,
612 Step::Fail(e) => return Err(e),
613 Step::Backoff(d) => {
614 attempt += 1;
615 tokio::time::sleep(d).await;
616 }
617 }
618 }
619
620 Ok(CaptureResponse {
621 results: final_results,
622 })
623 }
624
625 #[must_use = "feature flags result should be used"]
651 pub async fn get_feature_flags<S: Into<String>>(
652 &self,
653 distinct_id: S,
654 groups: Option<HashMap<String, String>>,
655 person_properties: Option<HashMap<String, serde_json::Value>>,
656 group_properties: Option<HashMap<String, HashMap<String, serde_json::Value>>>,
657 ) -> Result<
658 (
659 HashMap<String, FlagValue>,
660 HashMap<String, serde_json::Value>,
661 ),
662 Error,
663 > {
664 if self.options.is_disabled() {
665 trace!("Client is disabled, skipping feature flags request");
666 return Ok((HashMap::new(), HashMap::new()));
667 }
668
669 let flags_endpoint = self.options.endpoints().build_url(Endpoint::Flags);
670
671 let mut payload = json!({
672 "api_key": self.options.api_key,
673 "distinct_id": distinct_id.into(),
674 });
675
676 if let Some(groups) = groups {
677 payload["groups"] = json!(groups);
678 }
679
680 if let Some(person_properties) = person_properties {
681 payload["person_properties"] = json!(person_properties);
682 }
683
684 if let Some(group_properties) = group_properties {
685 payload["group_properties"] = json!(group_properties);
686 }
687
688 if self.options.disable_geoip {
690 payload["disable_geoip"] = json!(true);
691 }
692
693 let response = self
694 .client
695 .post(&flags_endpoint)
696 .header(CONTENT_TYPE, "application/json")
697 .header(USER_AGENT, get_default_user_agent())
698 .json(&payload)
699 .timeout(Duration::from_secs(
700 self.options.feature_flags_request_timeout_seconds,
701 ))
702 .send()
703 .await
704 .map_err(|e| Error::Connection(e.to_string()))?;
705
706 if !response.status().is_success() {
707 let status = response.status();
708 let text = response
709 .text()
710 .await
711 .unwrap_or_else(|_| "Unknown error".to_string());
712 return Err(Error::Connection(format!(
713 "API request failed with status {status}: {text}"
714 )));
715 }
716
717 let flags_response = response.json::<FeatureFlagsResponse>().await.map_err(|e| {
718 Error::Serialization(format!("Failed to parse feature flags response: {e}"))
719 })?;
720
721 Ok(flags_response.normalize())
722 }
723
724 #[must_use = "feature flag result should be used"]
745 #[instrument(skip_all, level = "debug")]
746 #[deprecated(
747 since = "0.6.0",
748 note = "Use Client::evaluate_flags() to fetch a snapshot, then call .get_flag(key) on it. \
749 The snapshot deduplicates $feature_flag_called events and supports attaching \
750 rich metadata to captured events via Event::with_flags()."
751 )]
752 pub async fn get_feature_flag<K: Into<String>, D: Into<String>>(
753 &self,
754 key: K,
755 distinct_id: D,
756 groups: Option<HashMap<String, String>>,
757 person_properties: Option<HashMap<String, serde_json::Value>>,
758 group_properties: Option<HashMap<String, HashMap<String, serde_json::Value>>>,
759 ) -> Result<Option<FlagValue>, Error> {
760 let key_str = key.into();
761 let distinct_id_str = distinct_id.into();
762
763 if let Some(ref evaluator) = self.local_evaluator {
765 let empty_props = HashMap::new();
766 let empty_groups: HashMap<String, String> = HashMap::new();
767 let empty_group_props: HashMap<String, HashMap<String, serde_json::Value>> =
768 HashMap::new();
769 let props = person_properties.as_ref().unwrap_or(&empty_props);
770 let groups_ref = groups.as_ref().unwrap_or(&empty_groups);
771 let group_props_ref = group_properties.as_ref().unwrap_or(&empty_group_props);
772 match evaluator.evaluate_flag(
773 &key_str,
774 &distinct_id_str,
775 props,
776 groups_ref,
777 group_props_ref,
778 ) {
779 Ok(Some(value)) => {
780 debug!(flag = %key_str, ?value, "Flag evaluated locally");
781 return Ok(Some(value));
782 }
783 Ok(None) => {
784 if self.options.local_evaluation_only {
785 debug!(flag = %key_str, "Flag not found locally, skipping remote fallback");
786 return Ok(None);
787 }
788 debug!(flag = %key_str, "Flag not found locally, falling back to API");
789 }
790 Err(e) => {
791 if self.options.local_evaluation_only {
792 debug!(flag = %key_str, error = %e.message, "Inconclusive local evaluation, skipping remote fallback");
793 return Ok(None);
794 }
795 debug!(flag = %key_str, error = %e.message, "Inconclusive local evaluation, falling back to API");
796 }
797 }
798 }
799
800 trace!(flag = %key_str, "Fetching flag from API");
802 let (feature_flags, _payloads) = self
803 .get_feature_flags(distinct_id_str, groups, person_properties, group_properties)
804 .await?;
805 Ok(feature_flags.get(&key_str).cloned())
806 }
807
808 #[must_use = "feature flag enabled check result should be used"]
819 #[deprecated(
820 since = "0.6.0",
821 note = "Use Client::evaluate_flags() to fetch a snapshot, then call .is_enabled(key) \
822 on it. The snapshot deduplicates $feature_flag_called events and supports \
823 attaching rich metadata to captured events via Event::with_flags()."
824 )]
825 #[allow(deprecated)] pub async fn is_feature_enabled<K: Into<String>, D: Into<String>>(
827 &self,
828 key: K,
829 distinct_id: D,
830 groups: Option<HashMap<String, String>>,
831 person_properties: Option<HashMap<String, serde_json::Value>>,
832 group_properties: Option<HashMap<String, HashMap<String, serde_json::Value>>>,
833 ) -> Result<bool, Error> {
834 let flag_value = self
835 .get_feature_flag(
836 key.into(),
837 distinct_id.into(),
838 groups,
839 person_properties,
840 group_properties,
841 )
842 .await?;
843 Ok(match flag_value {
844 Some(FlagValue::Boolean(b)) => b,
845 Some(FlagValue::String(_)) => true, None => false,
847 })
848 }
849
850 #[must_use = "feature flag payload result should be used"]
867 #[deprecated(
868 since = "0.6.0",
869 note = "Use Client::evaluate_flags() to fetch a snapshot, then call \
870 .get_flag_payload(key) on it. Reading the payload from a snapshot is \
871 event-free, matching this method's behavior, and avoids the per-call \
872 /flags request."
873 )]
874 pub async fn get_feature_flag_payload<K: Into<String>, D: Into<String>>(
875 &self,
876 key: K,
877 distinct_id: D,
878 ) -> Result<Option<serde_json::Value>, Error> {
879 if self.options.is_disabled() {
880 trace!("Client is disabled, skipping feature flag payload request");
881 return Ok(None);
882 }
883
884 let key_str = key.into();
885 let flags_endpoint = self.options.endpoints().build_url(Endpoint::Flags);
886
887 let mut payload = json!({
888 "api_key": self.options.api_key,
889 "distinct_id": distinct_id.into(),
890 });
891
892 if self.options.disable_geoip {
894 payload["disable_geoip"] = json!(true);
895 }
896
897 let response = self
898 .client
899 .post(&flags_endpoint)
900 .header(CONTENT_TYPE, "application/json")
901 .header(USER_AGENT, get_default_user_agent())
902 .json(&payload)
903 .timeout(Duration::from_secs(
904 self.options.feature_flags_request_timeout_seconds,
905 ))
906 .send()
907 .await
908 .map_err(|e| Error::Connection(e.to_string()))?;
909
910 if !response.status().is_success() {
911 return Ok(None);
912 }
913
914 let flags_response: FeatureFlagsResponse = response
915 .json()
916 .await
917 .map_err(|e| Error::Serialization(format!("Failed to parse response: {e}")))?;
918
919 let (_flags, payloads) = flags_response.normalize();
920 Ok(payloads.get(&key_str).cloned())
921 }
922
923 #[allow(clippy::too_many_arguments)]
943 pub fn evaluate_feature_flag_locally(
944 &self,
945 flag: &FeatureFlag,
946 distinct_id: &str,
947 person_properties: &HashMap<String, serde_json::Value>,
948 groups: &HashMap<String, String>,
949 group_properties: &HashMap<String, HashMap<String, serde_json::Value>>,
950 ) -> Result<FlagValue, Error> {
951 let group_type_mapping = self
952 .local_evaluator
953 .as_ref()
954 .map(|ev| ev.cache().get_group_type_mapping())
955 .unwrap_or_default();
956 match_feature_flag(
957 flag,
958 distinct_id,
959 person_properties,
960 groups,
961 group_properties,
962 &group_type_mapping,
963 )
964 .map_err(|e| Error::InconclusiveMatch(e.message))
965 }
966
967 pub async fn evaluate_flags<S: Into<String>>(
990 &self,
991 distinct_id: S,
992 options: EvaluateFlagsOptions,
993 ) -> Result<FeatureFlagEvaluations, Error> {
994 let distinct_id: String = distinct_id.into();
995 let host = self.flag_event_host();
996
997 if distinct_id.is_empty() || self.options.is_disabled() {
998 return Ok(FeatureFlagEvaluations::empty(host));
999 }
1000
1001 let mut records: HashMap<String, EvaluatedFlagRecord> = HashMap::new();
1002 let mut locally_evaluated_keys: HashSet<String> = HashSet::new();
1003
1004 if let Some(evaluator) = &self.local_evaluator {
1005 let person_props_owned = options.person_properties.clone().unwrap_or_default();
1006 let groups_owned = options.groups.clone().unwrap_or_default();
1007 let group_props_owned = options.group_properties.clone().unwrap_or_default();
1008 let local_results = evaluator.evaluate_all_flags(
1009 &distinct_id,
1010 &person_props_owned,
1011 &groups_owned,
1012 &group_props_owned,
1013 );
1014 for (key, result) in local_results {
1015 if let Some(filter) = &options.flag_keys {
1016 if !filter.iter().any(|k| k == &key) {
1017 continue;
1018 }
1019 }
1020 if let Ok(value) = result {
1021 records.insert(key.clone(), local_record(value));
1022 locally_evaluated_keys.insert(key);
1023 }
1024 }
1025 }
1026
1027 let mut request_id: Option<String> = None;
1028 let mut errors_while_computing = false;
1029 let mut quota_limited = false;
1030
1031 let local_covers_request = options
1036 .flag_keys
1037 .as_ref()
1038 .is_some_and(|keys| keys.iter().all(|k| locally_evaluated_keys.contains(k)));
1039
1040 if !options.only_evaluate_locally && !local_covers_request {
1041 match self.fetch_flag_details(&distinct_id, &options).await {
1046 Ok(response) => {
1047 request_id = response.request_id;
1048 errors_while_computing = response.errors_while_computing_flags;
1049 quota_limited = response.quota_limited;
1050 for (key, detail) in response.flags {
1051 if locally_evaluated_keys.contains(&key) {
1052 continue;
1053 }
1054 records.insert(key, remote_record_from_detail(detail));
1055 }
1056 }
1057 Err(e) => {
1058 if records.is_empty() {
1059 return Err(e);
1060 }
1061 debug!(
1062 error = e.to_string(),
1063 local_count = records.len(),
1064 "/flags fetch failed; returning snapshot from local results only"
1065 );
1066 errors_while_computing = true;
1067 }
1068 }
1069 }
1070
1071 Ok(FeatureFlagEvaluations::new(
1072 host,
1073 distinct_id,
1074 records,
1075 options.groups.unwrap_or_default(),
1076 options.disable_geoip,
1077 request_id,
1078 None,
1079 errors_while_computing,
1080 quota_limited,
1081 ))
1082 }
1083
1084 fn flag_event_host(&self) -> Arc<dyn FeatureFlagEvaluationsHost> {
1085 self.flag_event_host
1086 .get_or_init(|| {
1087 Arc::new(AsyncFlagEventHost::from_options(
1088 &self.options,
1089 self.client.clone(),
1090 )) as Arc<dyn FeatureFlagEvaluationsHost>
1091 })
1092 .clone()
1093 }
1094
1095 async fn fetch_flag_details(
1096 &self,
1097 distinct_id: &str,
1098 options: &EvaluateFlagsOptions,
1099 ) -> Result<DetailedFlagsResponse, Error> {
1100 let flags_endpoint = self.options.endpoints().build_url(Endpoint::Flags);
1101
1102 let mut payload = json!({
1103 "api_key": self.options.api_key,
1104 "distinct_id": distinct_id,
1105 });
1106 if let Some(groups) = &options.groups {
1107 payload["groups"] = json!(groups);
1108 }
1109 if let Some(person_properties) = &options.person_properties {
1110 payload["person_properties"] = json!(person_properties);
1111 }
1112 if let Some(group_properties) = &options.group_properties {
1113 payload["group_properties"] = json!(group_properties);
1114 }
1115 let effective_disable_geoip = options.disable_geoip.unwrap_or(self.options.disable_geoip);
1116 if effective_disable_geoip {
1117 payload["disable_geoip"] = json!(true);
1118 }
1119 if let Some(flag_keys) = &options.flag_keys {
1120 payload["flag_keys_to_evaluate"] = json!(flag_keys);
1121 }
1122
1123 let response = self
1124 .client
1125 .post(&flags_endpoint)
1126 .header(CONTENT_TYPE, "application/json")
1127 .header(USER_AGENT, get_default_user_agent())
1128 .json(&payload)
1129 .timeout(Duration::from_secs(
1130 self.options.feature_flags_request_timeout_seconds,
1131 ))
1132 .send()
1133 .await
1134 .map_err(|e| Error::Connection(e.to_string()))?;
1135
1136 if !response.status().is_success() {
1137 let status = response.status();
1138 let text = response
1139 .text()
1140 .await
1141 .unwrap_or_else(|_| "Unknown error".to_string());
1142 return Err(Error::Connection(format!(
1143 "API request failed with status {status}: {text}"
1144 )));
1145 }
1146
1147 let parsed = response.json::<FeatureFlagsResponse>().await.map_err(|e| {
1148 Error::Serialization(format!("Failed to parse feature flags response: {e}"))
1149 })?;
1150 Ok(extract_flag_details(parsed))
1151 }
1152}