1use std::collections::HashMap;
2use std::fmt;
3use std::sync::{Arc, LazyLock, Mutex as StdMutex, RwLock};
4use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
5
6use async_lock::Mutex;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10use crate::app;
11use crate::app::FirebaseApp;
12use crate::app_check::{AppCheckTokenError, AppCheckTokenResult, FirebaseAppCheckInternal};
13use crate::auth::FirebaseAuth;
14use crate::component::types::{
15 ComponentError, DynService, InstanceFactoryOptions, InstantiationMode,
16};
17use crate::component::{Component, ComponentType};
18use crate::installations::get_installations;
19use crate::performance::constants::{
20 MAX_ATTRIBUTE_NAME_LENGTH, MAX_ATTRIBUTE_VALUE_LENGTH, MAX_METRIC_NAME_LENGTH,
21 OOB_TRACE_PAGE_LOAD_PREFIX, PERFORMANCE_COMPONENT_NAME, RESERVED_ATTRIBUTE_PREFIXES,
22 RESERVED_METRIC_PREFIX,
23};
24use crate::performance::error::{internal_error, invalid_argument, PerformanceResult};
25use crate::performance::instrumentation;
26use crate::performance::storage::{create_trace_store, TraceEnvelope, TraceStoreHandle};
27use crate::performance::transport::{TransportController, TransportOptions};
28#[cfg(all(target_arch = "wasm32", feature = "wasm-web"))]
29use crate::platform::environment;
30use crate::platform::runtime;
31use log::debug;
32
33#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
39pub struct PerformanceSettings {
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub data_collection_enabled: Option<bool>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub instrumentation_enabled: Option<bool>,
44}
45
46#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct PerformanceRuntimeSettings {
50 data_collection_enabled: bool,
51 instrumentation_enabled: bool,
52}
53
54impl PerformanceRuntimeSettings {
55 fn resolve(app: &FirebaseApp, overrides: Option<&PerformanceSettings>) -> Self {
56 let mut resolved = Self {
57 data_collection_enabled: app.automatic_data_collection_enabled(),
58 instrumentation_enabled: app.automatic_data_collection_enabled(),
59 };
60
61 if let Some(settings) = overrides {
62 if let Some(value) = settings.data_collection_enabled {
63 resolved.data_collection_enabled = value;
64 }
65 if let Some(value) = settings.instrumentation_enabled {
66 resolved.instrumentation_enabled = value;
67 }
68 }
69
70 resolved
71 }
72
73 pub fn data_collection_enabled(&self) -> bool {
74 self.data_collection_enabled
75 }
76
77 pub fn instrumentation_enabled(&self) -> bool {
78 self.instrumentation_enabled
79 }
80}
81
82impl Default for PerformanceRuntimeSettings {
83 fn default() -> Self {
84 Self {
85 data_collection_enabled: true,
86 instrumentation_enabled: true,
87 }
88 }
89}
90
91#[derive(Clone)]
97pub struct Performance {
98 inner: Arc<PerformanceInner>,
99}
100
101struct PerformanceInner {
102 app: FirebaseApp,
103 traces: Mutex<HashMap<String, PerformanceTrace>>,
104 network_requests: Mutex<Vec<NetworkRequestRecord>>,
105 settings: RwLock<PerformanceRuntimeSettings>,
106 app_check: StdMutex<Option<FirebaseAppCheckInternal>>,
107 auth: StdMutex<Option<AuthContext>>,
108 trace_store: TraceStoreHandle,
109 transport: StdMutex<Option<Arc<TransportController>>>,
110 transport_options: Arc<RwLock<TransportOptions>>,
111 installation_id: Mutex<Option<String>>,
112}
113
114#[derive(Clone)]
116pub struct TraceHandle {
117 performance: Performance,
118 name: Arc<str>,
119 state: TraceLifecycle,
120 metrics: HashMap<String, i64>,
121 attributes: HashMap<String, String>,
122 is_auto: bool,
123}
124
125#[derive(Clone)]
128pub struct NetworkTraceHandle {
129 performance: Performance,
130 url: String,
131 method: HttpMethod,
132 state: NetworkLifecycle,
133 request_payload_bytes: Option<u64>,
134 response_payload_bytes: Option<u64>,
135 response_code: Option<u16>,
136 response_content_type: Option<String>,
137}
138
139#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
142pub enum HttpMethod {
143 Get,
144 Put,
145 Post,
146 Delete,
147 Head,
148 Patch,
149 Options,
150 Trace,
151 Connect,
152 Custom(String),
153}
154
155impl HttpMethod {
156 pub fn as_str(&self) -> &str {
158 match self {
159 HttpMethod::Get => "GET",
160 HttpMethod::Put => "PUT",
161 HttpMethod::Post => "POST",
162 HttpMethod::Delete => "DELETE",
163 HttpMethod::Head => "HEAD",
164 HttpMethod::Patch => "PATCH",
165 HttpMethod::Options => "OPTIONS",
166 HttpMethod::Trace => "TRACE",
167 HttpMethod::Connect => "CONNECT",
168 HttpMethod::Custom(value) => value.as_str(),
169 }
170 }
171}
172
173#[derive(Clone, Debug, PartialEq)]
175pub struct PerformanceTrace {
176 pub name: Arc<str>,
177 pub start_time_us: u128,
178 pub duration: Duration,
179 pub metrics: HashMap<String, i64>,
180 pub attributes: HashMap<String, String>,
181 pub is_auto: bool,
182 pub auth_uid: Option<String>,
183}
184
185#[derive(Clone, Debug, Default, PartialEq, Eq)]
188pub struct TraceRecordOptions {
189 pub metrics: HashMap<String, i64>,
190 pub attributes: HashMap<String, String>,
191}
192
193#[derive(Clone, Debug, PartialEq)]
196pub struct NetworkRequestRecord {
197 pub url: String,
198 pub http_method: HttpMethod,
199 pub start_time_us: u128,
200 pub time_to_request_completed_us: u128,
201 pub time_to_response_initiated_us: Option<u128>,
202 pub time_to_response_completed_us: Option<u128>,
203 pub request_payload_bytes: Option<u64>,
204 pub response_payload_bytes: Option<u64>,
205 pub response_code: Option<u16>,
206 pub response_content_type: Option<String>,
207 pub app_check_token: Option<String>,
208}
209
210#[derive(Clone, Debug)]
211enum TraceLifecycle {
212 Idle,
213 Running {
214 started_at: Instant,
215 started_micros: u128,
216 },
217 Completed,
218}
219
220#[derive(Clone, Debug)]
221enum NetworkLifecycle {
222 Idle,
223 Running {
224 started_at: Instant,
225 started_micros: u128,
226 response_initiated: Option<Duration>,
227 },
228 Completed,
229}
230
231#[derive(Clone)]
232enum AuthContext {
233 Firebase(FirebaseAuth),
234 Static(String),
235}
236
237impl AuthContext {
238 fn current_uid(&self) -> Option<String> {
239 match self {
240 AuthContext::Firebase(auth) => auth.current_user().map(|user| user.uid().to_string()),
241 AuthContext::Static(uid) if uid.is_empty() => None,
242 AuthContext::Static(uid) => Some(uid.clone()),
243 }
244 }
245}
246
247impl Performance {
248 fn new(app: FirebaseApp, settings: Option<PerformanceSettings>) -> Self {
249 let resolved = PerformanceRuntimeSettings::resolve(&app, settings.as_ref());
250 let trace_store = create_trace_store(&app);
251 let transport_options = Arc::new(RwLock::new(TransportOptions::default()));
252 let inner = PerformanceInner {
253 app,
254 traces: Mutex::new(HashMap::new()),
255 network_requests: Mutex::new(Vec::new()),
256 settings: RwLock::new(resolved),
257 app_check: StdMutex::new(None),
258 auth: StdMutex::new(None),
259 trace_store,
260 transport: StdMutex::new(None),
261 transport_options,
262 installation_id: Mutex::new(None),
263 };
264 let performance = Self {
265 inner: Arc::new(inner),
266 };
267 performance.initialize_background_tasks();
268 performance
269 }
270
271 fn initialize_background_tasks(&self) {
272 instrumentation::initialize(self);
273 let controller = TransportController::new(
274 self.clone(),
275 self.inner.trace_store.clone(),
276 self.inner.transport_options.clone(),
277 );
278 if let Ok(mut guard) = self.inner.transport.lock() {
279 *guard = Some(controller);
280 }
281 }
282
283 pub fn app(&self) -> &FirebaseApp {
285 &self.inner.app
286 }
287
288 pub fn settings(&self) -> PerformanceRuntimeSettings {
290 self.inner
291 .settings
292 .read()
293 .expect("settings lock poisoned")
294 .clone()
295 }
296
297 pub fn apply_settings(&self, settings: PerformanceSettings) {
299 let mut guard = self.inner.settings.write().expect("settings lock poisoned");
300 if let Some(value) = settings.data_collection_enabled {
301 guard.data_collection_enabled = value;
302 }
303 if let Some(value) = settings.instrumentation_enabled {
304 guard.instrumentation_enabled = value;
305 }
306 }
307
308 pub fn set_data_collection_enabled(&self, enabled: bool) {
310 self.apply_settings(PerformanceSettings {
311 data_collection_enabled: Some(enabled),
312 ..Default::default()
313 });
314 }
315
316 pub fn set_instrumentation_enabled(&self, enabled: bool) {
318 self.apply_settings(PerformanceSettings {
319 instrumentation_enabled: Some(enabled),
320 ..Default::default()
321 });
322 }
323
324 pub fn data_collection_enabled(&self) -> bool {
326 self.settings().data_collection_enabled()
327 }
328
329 pub fn instrumentation_enabled(&self) -> bool {
331 self.settings().instrumentation_enabled()
332 }
333
334 pub fn attach_app_check(&self, app_check: FirebaseAppCheckInternal) {
337 let mut guard = self.inner.app_check.lock().expect("app_check lock");
338 *guard = Some(app_check);
339 }
340
341 pub fn clear_app_check(&self) {
343 let mut guard = self.inner.app_check.lock().expect("app_check lock");
344 guard.take();
345 }
346
347 pub fn attach_auth(&self, auth: FirebaseAuth) {
350 let mut guard = self.inner.auth.lock().expect("auth lock");
351 *guard = Some(AuthContext::Firebase(auth));
352 }
353
354 pub fn set_authenticated_user_id(&self, uid: Option<&str>) {
357 let mut guard = self.inner.auth.lock().expect("auth lock");
358 match uid {
359 Some(value) => *guard = Some(AuthContext::Static(value.to_string())),
360 None => {
361 guard.take();
362 }
363 }
364 }
365
366 pub fn clear_auth(&self) {
368 let mut guard = self.inner.auth.lock().expect("auth lock");
369 guard.take();
370 }
371
372 pub fn new_trace(&self, name: &str) -> PerformanceResult<TraceHandle> {
387 validate_trace_name(name)?;
388 Ok(TraceHandle {
389 performance: self.clone(),
390 name: Arc::from(name.to_string()),
391 state: TraceLifecycle::Idle,
392 metrics: HashMap::new(),
393 attributes: HashMap::new(),
394 is_auto: false,
395 })
396 }
397
398 pub fn new_network_request(
413 &self,
414 url: &str,
415 method: HttpMethod,
416 ) -> PerformanceResult<NetworkTraceHandle> {
417 if url.trim().is_empty() {
418 return Err(invalid_argument("Request URL must not be empty"));
419 }
420 Ok(NetworkTraceHandle {
421 performance: self.clone(),
422 url: url.to_string(),
423 method,
424 state: NetworkLifecycle::Idle,
425 request_payload_bytes: None,
426 response_payload_bytes: None,
427 response_code: None,
428 response_content_type: None,
429 })
430 }
431
432 pub async fn recorded_trace(&self, name: &str) -> Option<PerformanceTrace> {
434 self.inner.traces.lock().await.get(name).cloned()
435 }
436
437 pub async fn recorded_network_request(&self, url: &str) -> Option<NetworkRequestRecord> {
440 let traces = self.inner.network_requests.lock().await;
441 traces
442 .iter()
443 .rev()
444 .find(|record| record.url == url)
445 .cloned()
446 }
447
448 pub fn configure_transport(&self, options: TransportOptions) {
450 if let Ok(mut guard) = self.inner.transport_options.write() {
451 *guard = options;
452 }
453 if let Some(controller) = self.transport_controller() {
454 controller.trigger_flush();
455 }
456 }
457
458 pub async fn flush_transport(&self) -> PerformanceResult<()> {
460 if let Some(controller) = self.transport_controller() {
461 controller.flush_once().await
462 } else {
463 Ok(())
464 }
465 }
466
467 #[cfg_attr(
468 not(all(feature = "wasm-web", target_arch = "wasm32")),
469 allow(dead_code)
470 )]
471 pub(crate) async fn record_auto_trace(
472 self,
473 name: &str,
474 start_time_us: u128,
475 duration: Duration,
476 metrics: HashMap<String, i64>,
477 attributes: HashMap<String, String>,
478 ) -> PerformanceResult<()> {
479 let trace = PerformanceTrace {
480 name: Arc::from(name.to_string()),
481 start_time_us,
482 duration,
483 metrics,
484 attributes,
485 is_auto: true,
486 auth_uid: self.auth_uid(),
487 };
488 self.store_trace(trace).await
489 }
490
491 #[cfg_attr(
492 not(all(feature = "wasm-web", target_arch = "wasm32")),
493 allow(dead_code)
494 )]
495 pub(crate) async fn record_auto_network(
496 self,
497 record: NetworkRequestRecord,
498 ) -> PerformanceResult<()> {
499 self.store_network_request(record).await
500 }
501
502 pub(crate) async fn installation_id(&self) -> Option<String> {
503 if let Some(cached) = self.inner.installation_id.lock().await.clone() {
504 return Some(cached);
505 }
506 let installations = get_installations(Some(self.app().clone())).ok()?;
507 let fid = installations.get_id().await.ok()?;
508 let mut guard = self.inner.installation_id.lock().await;
509 *guard = Some(fid.clone());
510 Some(fid)
511 }
512
513 async fn store_trace(&self, trace: PerformanceTrace) -> PerformanceResult<()> {
514 if !self.data_collection_enabled() {
515 return Ok(());
516 }
517 let name = trace.name.to_string();
518 self.inner
519 .traces
520 .lock()
521 .await
522 .insert(name.clone(), trace.clone());
523 if let Err(err) = self
524 .inner
525 .trace_store
526 .push(TraceEnvelope::Trace(trace))
527 .await
528 {
529 debug!("failed to persist trace {name}: {}", err);
530 }
531 if let Some(controller) = self.transport_controller() {
532 controller.trigger_flush();
533 }
534 Ok(())
535 }
536
537 async fn store_network_request(&self, record: NetworkRequestRecord) -> PerformanceResult<()> {
538 if !self.instrumentation_enabled() {
539 return Ok(());
540 }
541 const MAX_HISTORY: usize = 50;
542 let mut traces = self.inner.network_requests.lock().await;
543 if traces.len() == MAX_HISTORY {
544 traces.remove(0);
545 }
546 traces.push(record.clone());
547 if let Err(err) = self
548 .inner
549 .trace_store
550 .push(TraceEnvelope::Network(record))
551 .await
552 {
553 debug!("failed to persist network trace: {}", err);
554 }
555 if let Some(controller) = self.transport_controller() {
556 controller.trigger_flush();
557 }
558 Ok(())
559 }
560
561 fn auth_uid(&self) -> Option<String> {
562 let ctx = self.inner.auth.lock().ok().and_then(|guard| guard.clone());
563 ctx.and_then(|ctx| ctx.current_uid())
564 }
565
566 fn transport_controller(&self) -> Option<Arc<TransportController>> {
567 self.inner
568 .transport
569 .lock()
570 .ok()
571 .and_then(|guard| guard.as_ref().cloned())
572 }
573
574 async fn app_check_token(&self) -> Option<String> {
575 let provider = self
576 .inner
577 .app_check
578 .lock()
579 .ok()
580 .and_then(|guard| guard.clone())?;
581 match provider.get_token(false).await {
582 Ok(result) => normalize_token_result(result),
583 Err(err) => cached_token_from_error(&err),
584 }
585 }
586}
587
588impl fmt::Debug for Performance {
589 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
590 f.debug_struct("Performance")
591 .field("app", self.app())
592 .finish()
593 }
594}
595
596fn normalize_token_result(result: AppCheckTokenResult) -> Option<String> {
597 if result.token.is_empty() {
598 None
599 } else {
600 Some(result.token)
601 }
602}
603
604fn cached_token_from_error(error: &AppCheckTokenError) -> Option<String> {
605 error.cached_token().and_then(|token| {
606 if token.token.is_empty() {
607 None
608 } else {
609 Some(token.token.clone())
610 }
611 })
612}
613
614impl TraceHandle {
615 pub fn start(&mut self) -> PerformanceResult<()> {
617 match self.state {
618 TraceLifecycle::Idle => {
619 self.state = TraceLifecycle::Running {
620 started_at: Instant::now(),
621 started_micros: now_micros(),
622 };
623 Ok(())
624 }
625 TraceLifecycle::Running { .. } => {
626 Err(invalid_argument("Trace has already been started"))
627 }
628 TraceLifecycle::Completed => Err(invalid_argument("Trace has already completed")),
629 }
630 }
631
632 pub fn put_metric(&mut self, name: &str, value: i64) -> PerformanceResult<()> {
634 validate_metric_name(name, &self.name)?;
635 self.metrics.insert(name.to_string(), value);
636 Ok(())
637 }
638
639 pub fn increment_metric(&mut self, name: &str, delta: i64) -> PerformanceResult<()> {
641 validate_metric_name(name, &self.name)?;
642 let entry = self.metrics.entry(name.to_string()).or_insert(0);
643 *entry = entry.saturating_add(delta);
644 Ok(())
645 }
646
647 pub fn get_metric(&self, name: &str) -> i64 {
649 *self.metrics.get(name).unwrap_or(&0)
650 }
651
652 pub fn put_attribute(&mut self, name: &str, value: &str) -> PerformanceResult<()> {
654 validate_attribute_name(name)?;
655 validate_attribute_value(value)?;
656 self.attributes.insert(name.to_string(), value.to_string());
657 Ok(())
658 }
659
660 pub fn remove_attribute(&mut self, name: &str) {
662 self.attributes.remove(name);
663 }
664
665 pub fn get_attribute(&self, name: &str) -> Option<&str> {
667 self.attributes.get(name).map(|value| value.as_str())
668 }
669
670 pub fn attributes(&self) -> &HashMap<String, String> {
672 &self.attributes
673 }
674
675 pub async fn record(
691 &self,
692 start_time: SystemTime,
693 duration: Duration,
694 options: Option<TraceRecordOptions>,
695 ) -> PerformanceResult<PerformanceTrace> {
696 if duration.is_zero() {
697 return Err(invalid_argument("Trace duration must be positive"));
698 }
699 let mut metrics = HashMap::new();
700 let mut attributes = HashMap::new();
701 if let Some(opts) = options {
702 metrics = opts.metrics;
703 attributes = opts.attributes;
704 }
705 let trace = PerformanceTrace {
706 name: self.name.clone(),
707 start_time_us: timestamp_micros(start_time),
708 duration,
709 metrics,
710 attributes,
711 is_auto: self.is_auto,
712 auth_uid: self.performance.auth_uid(),
713 };
714 self.performance.store_trace(trace.clone()).await?;
715 Ok(trace)
716 }
717
718 pub async fn stop(mut self) -> PerformanceResult<PerformanceTrace> {
720 let (started_at, started_micros) = match self.state {
721 TraceLifecycle::Running {
722 started_at,
723 started_micros,
724 } => (started_at, started_micros),
725 TraceLifecycle::Idle => {
726 return Err(invalid_argument("Trace must be started before stopping"))
727 }
728 TraceLifecycle::Completed => return Err(invalid_argument("Trace already completed")),
729 };
730 self.state = TraceLifecycle::Completed;
731 let trace = PerformanceTrace {
732 name: self.name.clone(),
733 start_time_us: started_micros,
734 duration: started_at.elapsed(),
735 metrics: self.metrics.clone(),
736 attributes: self.attributes.clone(),
737 is_auto: self.is_auto,
738 auth_uid: self.performance.auth_uid(),
739 };
740 self.performance.store_trace(trace.clone()).await?;
741 Ok(trace)
742 }
743}
744
745impl NetworkTraceHandle {
746 pub fn start(&mut self) -> PerformanceResult<()> {
748 match self.state {
749 NetworkLifecycle::Idle => {
750 self.state = NetworkLifecycle::Running {
751 started_at: Instant::now(),
752 started_micros: now_micros(),
753 response_initiated: None,
754 };
755 Ok(())
756 }
757 NetworkLifecycle::Running { .. } => {
758 Err(invalid_argument("Network trace already started"))
759 }
760 NetworkLifecycle::Completed => Err(invalid_argument("Network trace already completed")),
761 }
762 }
763
764 pub fn mark_response_initiated(&mut self) -> PerformanceResult<()> {
766 match &mut self.state {
767 NetworkLifecycle::Running {
768 response_initiated,
769 started_at,
770 ..
771 } => {
772 if response_initiated.is_some() {
773 Err(invalid_argument("Response already marked as initiated"))
774 } else {
775 *response_initiated = Some(started_at.elapsed());
776 Ok(())
777 }
778 }
779 _ => Err(invalid_argument("Response can only be marked after start")),
780 }
781 }
782
783 pub fn set_request_payload_bytes(&mut self, bytes: u64) {
785 self.request_payload_bytes = Some(bytes);
786 }
787
788 pub fn set_response_payload_bytes(&mut self, bytes: u64) {
790 self.response_payload_bytes = Some(bytes);
791 }
792
793 pub fn set_response_code(&mut self, code: u16) -> PerformanceResult<()> {
795 if (100..=599).contains(&code) {
796 self.response_code = Some(code);
797 Ok(())
798 } else {
799 Err(invalid_argument(
800 "HTTP status code must be between 100 and 599",
801 ))
802 }
803 }
804
805 pub fn set_response_content_type(&mut self, content_type: impl Into<String>) {
807 self.response_content_type = Some(content_type.into());
808 }
809
810 pub async fn stop(mut self) -> PerformanceResult<NetworkRequestRecord> {
812 let (started_at, started_micros, response_initiated) = match &self.state {
813 NetworkLifecycle::Running {
814 started_at,
815 started_micros,
816 response_initiated,
817 } => (*started_at, *started_micros, *response_initiated),
818 NetworkLifecycle::Idle => {
819 return Err(invalid_argument(
820 "Network trace must be started before stopping",
821 ))
822 }
823 NetworkLifecycle::Completed => {
824 return Err(invalid_argument("Network trace already completed"))
825 }
826 };
827 self.state = NetworkLifecycle::Completed;
828 let duration = started_at.elapsed();
829 let response_initiated_us = response_initiated.map(|value| value.as_micros());
830 let record = NetworkRequestRecord {
831 url: self.url.clone(),
832 http_method: self.method.clone(),
833 start_time_us: started_micros,
834 time_to_request_completed_us: duration.as_micros(),
835 time_to_response_initiated_us: response_initiated_us,
836 time_to_response_completed_us: Some(duration.as_micros()),
837 request_payload_bytes: self.request_payload_bytes,
838 response_payload_bytes: self.response_payload_bytes,
839 response_code: self.response_code,
840 response_content_type: self.response_content_type.clone(),
841 app_check_token: self.performance.app_check_token().await,
842 };
843 self.performance
844 .store_network_request(record.clone())
845 .await?;
846 Ok(record)
847 }
848}
849
850fn validate_trace_name(name: &str) -> PerformanceResult<()> {
851 if name.trim().is_empty() {
852 Err(invalid_argument("Trace name must not be empty"))
853 } else {
854 Ok(())
855 }
856}
857
858fn validate_metric_name(name: &str, trace_name: &str) -> PerformanceResult<()> {
859 if name.is_empty() || name.len() > MAX_METRIC_NAME_LENGTH {
860 return Err(invalid_argument("Metric name must be 1-100 characters"));
861 }
862 if name.starts_with(RESERVED_METRIC_PREFIX)
863 && !trace_name.starts_with(OOB_TRACE_PAGE_LOAD_PREFIX)
864 {
865 return Err(invalid_argument(
866 "Metric names starting with '_' are reserved for auto traces",
867 ));
868 }
869 Ok(())
870}
871
872fn validate_attribute_name(name: &str) -> PerformanceResult<()> {
873 if name.is_empty() || name.len() > MAX_ATTRIBUTE_NAME_LENGTH {
874 return Err(invalid_argument("Attribute name must be 1-40 characters"));
875 }
876 if !name
877 .chars()
878 .next()
879 .map(|ch| ch.is_ascii_alphabetic())
880 .unwrap_or(false)
881 {
882 return Err(invalid_argument(
883 "Attribute names must start with an ASCII letter",
884 ));
885 }
886 if !name
887 .chars()
888 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
889 {
890 return Err(invalid_argument(
891 "Attribute names may only contain letters, numbers, and '_'",
892 ));
893 }
894 if RESERVED_ATTRIBUTE_PREFIXES
895 .iter()
896 .any(|prefix| name.starts_with(prefix))
897 {
898 return Err(invalid_argument("Attribute prefix is reserved"));
899 }
900 Ok(())
901}
902
903fn validate_attribute_value(value: &str) -> PerformanceResult<()> {
904 if value.is_empty() || value.len() > MAX_ATTRIBUTE_VALUE_LENGTH {
905 Err(invalid_argument("Attribute value must be 1-100 characters"))
906 } else {
907 Ok(())
908 }
909}
910
911fn timestamp_micros(time: SystemTime) -> u128 {
912 time.duration_since(UNIX_EPOCH)
913 .map(|duration| duration.as_micros())
914 .unwrap_or(0)
915}
916
917fn now_micros() -> u128 {
918 timestamp_micros(runtime::now())
919}
920
921pub fn is_supported() -> bool {
924 #[cfg(all(target_arch = "wasm32", feature = "wasm-web"))]
925 {
926 environment::is_browser()
927 }
928
929 #[cfg(not(all(target_arch = "wasm32", feature = "wasm-web")))]
930 {
931 true
932 }
933}
934
935static PERFORMANCE_COMPONENT: LazyLock<()> = LazyLock::new(|| {
936 let component = Component::new(
937 PERFORMANCE_COMPONENT_NAME,
938 Arc::new(performance_factory),
939 ComponentType::Public,
940 )
941 .with_instantiation_mode(InstantiationMode::Lazy);
942 let _ = app::register_component(component);
943});
944
945fn performance_factory(
946 container: &crate::component::ComponentContainer,
947 options: InstanceFactoryOptions,
948) -> Result<DynService, ComponentError> {
949 let app = container.root_service::<FirebaseApp>().ok_or_else(|| {
950 ComponentError::InitializationFailed {
951 name: PERFORMANCE_COMPONENT_NAME.to_string(),
952 reason: "Firebase app not attached to component container".to_string(),
953 }
954 })?;
955
956 let settings = match options.options {
957 Value::Null => None,
958 value => Some(serde_json::from_value(value).map_err(|err| {
959 ComponentError::InitializationFailed {
960 name: PERFORMANCE_COMPONENT_NAME.to_string(),
961 reason: format!("invalid settings: {err}"),
962 }
963 })?),
964 };
965
966 let performance = Performance::new((*app).clone(), settings);
967 Ok(Arc::new(performance) as DynService)
968}
969
970fn ensure_registered() {
971 LazyLock::force(&PERFORMANCE_COMPONENT);
972}
973
974pub fn register_performance_component() {
976 ensure_registered();
977}
978
979pub async fn initialize_performance(
983 app: FirebaseApp,
984 settings: Option<PerformanceSettings>,
985) -> PerformanceResult<Arc<Performance>> {
986 ensure_registered();
987 let provider = app::get_provider(&app, PERFORMANCE_COMPONENT_NAME);
988 let options_value = match settings {
989 Some(settings) => serde_json::to_value(&settings)
990 .map_err(|err| internal_error(format!("failed to serialize settings: {err}")))?,
991 None => Value::Null,
992 };
993 provider
994 .initialize::<Performance>(options_value, None)
995 .map_err(|err| match err {
996 ComponentError::InstanceAlreadyInitialized { .. } => {
997 invalid_argument("Performance has already been initialized for this app")
998 }
999 other => internal_error(other.to_string()),
1000 })
1001}
1002
1003pub async fn get_performance(app: Option<FirebaseApp>) -> PerformanceResult<Arc<Performance>> {
1006 ensure_registered();
1007 let app = match app {
1008 Some(app) => app,
1009 None => crate::app::get_app(None)
1010 .await
1011 .map_err(|err| internal_error(err.to_string()))?,
1012 };
1013
1014 let provider = app::get_provider(&app, PERFORMANCE_COMPONENT_NAME);
1015 if let Some(perf) = provider.get_immediate::<Performance>() {
1016 return Ok(perf);
1017 }
1018
1019 match provider.initialize::<Performance>(Value::Null, None) {
1020 Ok(perf) => Ok(perf),
1021 Err(ComponentError::InstanceUnavailable { .. }) => provider
1022 .get_immediate::<Performance>()
1023 .ok_or_else(|| internal_error("Performance component not available")),
1024 Err(err) => Err(internal_error(err.to_string())),
1025 }
1026}
1027
1028#[cfg(all(test, not(target_arch = "wasm32")))]
1029mod tests {
1030 use super::*;
1031 use crate::app::initialize_app;
1032 use crate::app::{FirebaseAppSettings, FirebaseOptions};
1033 use crate::app_check::{
1034 box_app_check_future, initialize_app_check, AppCheckOptions, AppCheckProvider,
1035 AppCheckProviderFuture, AppCheckResult, AppCheckToken,
1036 };
1037 use crate::performance::TransportOptions;
1038 use httpmock::prelude::*;
1039 use std::sync::Arc;
1040 use tokio::time::sleep;
1041
1042 fn disable_transport() {
1043 std::env::set_var("FIREBASE_PERF_DISABLE_TRANSPORT", "1");
1044 }
1045
1046 async fn test_app(name: &str) -> FirebaseApp {
1047 let options = FirebaseOptions {
1048 project_id: Some("project".into()),
1049 ..Default::default()
1050 };
1051 initialize_app(
1052 options,
1053 Some(FirebaseAppSettings {
1054 name: Some(name.to_string()),
1055 ..Default::default()
1056 }),
1057 )
1058 .await
1059 .expect("create app")
1060 }
1061
1062 #[tokio::test(flavor = "current_thread")]
1063 async fn trace_records_metrics_and_attributes() {
1064 disable_transport();
1065 let app = test_app("perf-trace").await;
1066 let performance = get_performance(Some(app)).await.unwrap();
1067 let mut trace = performance.new_trace("load").unwrap();
1068 trace.put_metric("items", 3).unwrap();
1069 trace.increment_metric("items", 2).unwrap();
1070 trace.put_attribute("locale", "en-US").unwrap();
1071 trace.start().unwrap();
1072 sleep(Duration::from_millis(5)).await;
1073 let result = trace.stop().await.unwrap();
1074 assert_eq!(result.metrics.get("items"), Some(&5));
1075 assert_eq!(result.attributes.get("locale"), Some(&"en-US".to_string()));
1076 }
1077
1078 #[tokio::test(flavor = "current_thread")]
1079 async fn record_api_stores_trace_once() {
1080 disable_transport();
1081 let app = test_app("perf-record").await;
1082 let performance = get_performance(Some(app)).await.unwrap();
1083 let trace = performance.new_trace("bootstrap").unwrap();
1084 let start = runtime::now();
1085 let trace = trace
1086 .record(start, Duration::from_millis(10), None)
1087 .await
1088 .unwrap();
1089 assert_eq!(trace.duration.as_millis(), 10);
1090 let stored = performance.recorded_trace("bootstrap").await.unwrap();
1091 assert_eq!(stored.duration.as_millis(), 10);
1092 }
1093
1094 #[tokio::test(flavor = "current_thread")]
1095 async fn network_request_collects_payload_and_status() {
1096 disable_transport();
1097 let app = test_app("perf-network").await;
1098 let performance = get_performance(Some(app)).await.unwrap();
1099 let mut request = performance
1100 .new_network_request("https://example.com", HttpMethod::Post)
1101 .unwrap();
1102 request.start().unwrap();
1103 sleep(Duration::from_millis(5)).await;
1104 request.mark_response_initiated().unwrap();
1105 request.set_request_payload_bytes(512);
1106 request.set_response_payload_bytes(1024);
1107 request.set_response_code(200).unwrap();
1108 request.set_response_content_type("application/json");
1109 let record = request.stop().await.unwrap();
1110 assert_eq!(record.response_code, Some(200));
1111 assert_eq!(record.request_payload_bytes, Some(512));
1112 assert!(record.time_to_request_completed_us >= 5_000);
1113 }
1114
1115 #[derive(Clone)]
1116 struct StaticAppCheckProvider;
1117
1118 impl AppCheckProvider for StaticAppCheckProvider {
1119 fn get_token(&self) -> AppCheckProviderFuture<'_, AppCheckResult<AppCheckToken>> {
1120 box_app_check_future(async move {
1121 AppCheckToken::with_ttl("app-check-token", Duration::from_secs(60))
1122 })
1123 }
1124 }
1125
1126 async fn attach_app_check(performance: &Performance, app: &FirebaseApp) {
1127 let options = AppCheckOptions::new(Arc::new(StaticAppCheckProvider));
1128 let app_check = initialize_app_check(Some(app.clone()), options)
1129 .await
1130 .expect("initialize app check");
1131 performance.attach_app_check(FirebaseAppCheckInternal::new(app_check));
1132 }
1133
1134 #[tokio::test(flavor = "current_thread")]
1135 async fn network_request_includes_app_check_token() {
1136 disable_transport();
1137 let app = test_app("perf-app-check").await;
1138 let performance = get_performance(Some(app.clone())).await.unwrap();
1139 attach_app_check(&performance, &app).await;
1140 let mut request = performance
1141 .new_network_request("https://example.com", HttpMethod::Get)
1142 .unwrap();
1143 request.start().unwrap();
1144 let record = request.stop().await.unwrap();
1145 assert_eq!(record.app_check_token.as_deref(), Some("app-check-token"));
1146 }
1147
1148 #[tokio::test(flavor = "current_thread")]
1149 async fn initialize_performance_respects_settings() {
1150 disable_transport();
1151 let app = test_app("perf-init").await;
1152 let settings = PerformanceSettings {
1153 data_collection_enabled: Some(false),
1154 instrumentation_enabled: Some(false),
1155 };
1156 let perf = initialize_performance(app.clone(), Some(settings.clone()))
1157 .await
1158 .unwrap();
1159 assert!(!perf.data_collection_enabled());
1160 assert!(!perf.instrumentation_enabled());
1161
1162 let err = initialize_performance(app, Some(settings))
1163 .await
1164 .unwrap_err();
1165 assert_eq!(err.code_str(), "performance/invalid-argument");
1166 }
1167
1168 #[cfg_attr(target_os = "linux", ignore = "localhost sockets disabled in sandbox")]
1169 #[tokio::test(flavor = "current_thread")]
1170 async fn transport_flushes_to_custom_endpoint() {
1171 std::env::remove_var("FIREBASE_PERF_DISABLE_TRANSPORT");
1172 let server = MockServer::start();
1173 let mock = server.mock(|when, then| {
1174 when.method(POST).path("/upload");
1175 then.status(200);
1176 });
1177
1178 let app = test_app("perf-transport").await;
1179 let performance = get_performance(Some(app)).await.unwrap();
1180 performance.configure_transport(TransportOptions {
1181 endpoint: Some(server.url("/upload")),
1182 api_key: None,
1183 flush_interval: Some(Duration::from_millis(10)),
1184 max_batch_size: Some(1),
1185 });
1186
1187 let mut trace = performance.new_trace("upload").unwrap();
1188 trace.start().unwrap();
1189 trace.stop().await.unwrap();
1190 performance.flush_transport().await.unwrap();
1191 sleep(Duration::from_millis(50)).await;
1192 mock.assert_hits(1);
1193 disable_transport();
1194 }
1195}