1use std::time::{SystemTime, UNIX_EPOCH};
33
34#[derive(Debug, Clone)]
36pub struct TracingConfig {
37 pub enabled: bool,
39 pub service_name: String,
41 pub trace_id: Option<String>,
43 pub sample_rate: f64,
45 pub capture_console: bool,
47 pub capture_network: bool,
49 pub capture_interactions: bool,
51}
52
53impl Default for TracingConfig {
54 fn default() -> Self {
55 Self {
56 enabled: true,
57 service_name: "probar-browser-test".to_string(),
58 trace_id: None,
59 sample_rate: 1.0,
60 capture_console: true,
61 capture_network: true,
62 capture_interactions: true,
63 }
64 }
65}
66
67impl TracingConfig {
68 #[must_use]
70 pub fn new(service_name: impl Into<String>) -> Self {
71 Self {
72 service_name: service_name.into(),
73 ..Default::default()
74 }
75 }
76
77 #[must_use]
79 pub const fn disabled() -> Self {
80 Self {
81 enabled: false,
82 service_name: String::new(),
83 trace_id: None,
84 sample_rate: 0.0,
85 capture_console: false,
86 capture_network: false,
87 capture_interactions: false,
88 }
89 }
90
91 #[must_use]
93 pub fn with_service_name(mut self, name: impl Into<String>) -> Self {
94 self.service_name = name.into();
95 self
96 }
97
98 #[must_use]
100 pub fn with_trace_id(mut self, id: impl Into<String>) -> Self {
101 self.trace_id = Some(id.into());
102 self
103 }
104
105 #[must_use]
107 pub fn with_sample_rate(mut self, rate: f64) -> Self {
108 self.sample_rate = rate.clamp(0.0, 1.0);
109 self
110 }
111
112 #[must_use]
114 pub const fn with_console_capture(mut self, enabled: bool) -> Self {
115 self.capture_console = enabled;
116 self
117 }
118
119 #[must_use]
121 pub const fn with_network_capture(mut self, enabled: bool) -> Self {
122 self.capture_network = enabled;
123 self
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct TraceContext {
130 pub trace_id: String,
132 pub parent_id: String,
134 pub flags: u8,
136}
137
138impl TraceContext {
139 #[must_use]
141 pub fn new() -> Self {
142 let trace_id = generate_trace_id();
143 let parent_id = generate_span_id();
144 Self {
145 trace_id,
146 parent_id,
147 flags: 0x01, }
149 }
150
151 #[must_use]
153 pub fn with_trace_id(trace_id: impl Into<String>) -> Self {
154 Self {
155 trace_id: trace_id.into(),
156 parent_id: generate_span_id(),
157 flags: 0x01,
158 }
159 }
160
161 pub fn parse(traceparent: &str) -> Option<Self> {
165 let parts: Vec<&str> = traceparent.split('-').collect();
166 if parts.len() != 4 {
167 return None;
168 }
169
170 if parts[0] != "00" {
172 return None;
173 }
174
175 if parts[1].len() != 32 {
177 return None;
178 }
179
180 if parts[2].len() != 16 {
182 return None;
183 }
184
185 let flags = u8::from_str_radix(parts[3], 16).ok()?;
187
188 Some(Self {
189 trace_id: parts[1].to_string(),
190 parent_id: parts[2].to_string(),
191 flags,
192 })
193 }
194
195 #[must_use]
197 pub fn to_traceparent(&self) -> String {
198 format!("00-{}-{}-{:02x}", self.trace_id, self.parent_id, self.flags)
199 }
200
201 #[must_use]
203 pub fn child(&self) -> Self {
204 Self {
205 trace_id: self.trace_id.clone(),
206 parent_id: generate_span_id(),
207 flags: self.flags,
208 }
209 }
210
211 #[must_use]
213 pub const fn is_sampled(&self) -> bool {
214 self.flags & 0x01 != 0
215 }
216}
217
218impl Default for TraceContext {
219 fn default() -> Self {
220 Self::new()
221 }
222}
223
224impl std::fmt::Display for TraceContext {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 write!(f, "{}", self.to_traceparent())
227 }
228}
229
230#[derive(Debug, Clone)]
232pub struct TraceSpan {
233 pub name: String,
235 pub context: TraceContext,
237 pub start_us: u64,
239 pub end_us: Option<u64>,
241 pub category: String,
243 pub attributes: Vec<(String, String)>,
245 pub children: Vec<TraceSpan>,
247}
248
249impl TraceSpan {
250 #[must_use]
252 pub fn new(name: impl Into<String>, category: impl Into<String>) -> Self {
253 Self {
254 name: name.into(),
255 context: TraceContext::new(),
256 start_us: now_micros(),
257 end_us: None,
258 category: category.into(),
259 attributes: Vec::new(),
260 children: Vec::new(),
261 }
262 }
263
264 #[must_use]
266 pub fn with_context(
267 name: impl Into<String>,
268 category: impl Into<String>,
269 context: TraceContext,
270 ) -> Self {
271 Self {
272 name: name.into(),
273 context,
274 start_us: now_micros(),
275 end_us: None,
276 category: category.into(),
277 attributes: Vec::new(),
278 children: Vec::new(),
279 }
280 }
281
282 pub fn add_attribute(&mut self, key: impl Into<String>, value: impl Into<String>) {
284 self.attributes.push((key.into(), value.into()));
285 }
286
287 pub fn end(&mut self) {
289 if self.end_us.is_none() {
290 self.end_us = Some(now_micros());
291 }
292 }
293
294 #[must_use]
296 pub fn duration_us(&self) -> Option<u64> {
297 self.end_us.map(|end| end.saturating_sub(self.start_us))
298 }
299
300 pub fn add_child(&mut self, child: TraceSpan) {
302 self.children.push(child);
303 }
304}
305
306#[derive(Debug, Clone, serde::Serialize)]
308pub struct ChromeTraceEvent {
309 pub name: String,
311 pub cat: String,
313 pub ph: String,
315 pub ts: u64,
317 #[serde(skip_serializing_if = "Option::is_none")]
319 pub dur: Option<u64>,
320 pub pid: u32,
322 pub tid: u32,
324 #[serde(skip_serializing_if = "Option::is_none")]
326 pub args: Option<serde_json::Value>,
327}
328
329impl ChromeTraceEvent {
330 pub fn from_span(span: &TraceSpan, pid: u32, tid: u32) -> Self {
332 let args = if span.attributes.is_empty() {
333 None
334 } else {
335 let map: serde_json::Map<String, serde_json::Value> = span
336 .attributes
337 .iter()
338 .map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
339 .collect();
340 Some(serde_json::Value::Object(map))
341 };
342
343 Self {
344 name: span.name.clone(),
345 cat: span.category.clone(),
346 ph: "X".to_string(),
347 ts: span.start_us,
348 dur: span.duration_us(),
349 pid,
350 tid,
351 args,
352 }
353 }
354
355 pub fn instant(
357 name: impl Into<String>,
358 category: impl Into<String>,
359 ts: u64,
360 pid: u32,
361 tid: u32,
362 ) -> Self {
363 Self {
364 name: name.into(),
365 cat: category.into(),
366 ph: "I".to_string(),
367 ts,
368 dur: None,
369 pid,
370 tid,
371 args: None,
372 }
373 }
374}
375
376#[derive(Debug, serde::Serialize)]
378pub struct ChromeTrace {
379 #[serde(rename = "traceEvents")]
381 pub trace_events: Vec<ChromeTraceEvent>,
382 #[serde(skip_serializing_if = "Option::is_none")]
384 pub metadata: Option<serde_json::Value>,
385}
386
387impl ChromeTrace {
388 #[must_use]
390 pub fn new() -> Self {
391 Self {
392 trace_events: Vec::new(),
393 metadata: None,
394 }
395 }
396
397 pub fn add_span(&mut self, span: &TraceSpan, pid: u32, tid: u32) {
399 self.trace_events
400 .push(ChromeTraceEvent::from_span(span, pid, tid));
401
402 for child in &span.children {
403 self.add_span(child, pid, tid + 1);
404 }
405 }
406
407 pub fn add_instant(
409 &mut self,
410 name: impl Into<String>,
411 category: impl Into<String>,
412 ts: u64,
413 pid: u32,
414 tid: u32,
415 ) {
416 self.trace_events
417 .push(ChromeTraceEvent::instant(name, category, ts, pid, tid));
418 }
419
420 pub fn set_metadata(&mut self, metadata: serde_json::Value) {
422 self.metadata = Some(metadata);
423 }
424
425 pub fn to_json(&self) -> Result<String, serde_json::Error> {
431 serde_json::to_string_pretty(self)
432 }
433
434 pub fn to_json_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
440 serde_json::to_vec_pretty(self)
441 }
442}
443
444impl Default for ChromeTrace {
445 fn default() -> Self {
446 Self::new()
447 }
448}
449
450#[derive(Debug, Default)]
452pub struct TraceCollector {
453 pub root_context: Option<TraceContext>,
455 pub spans: Vec<TraceSpan>,
457 pub console_timestamps: Vec<(u64, String)>,
459 pub service_name: String,
461}
462
463impl TraceCollector {
464 #[must_use]
466 pub fn new(service_name: impl Into<String>) -> Self {
467 let root_context = TraceContext::new();
468 Self {
469 root_context: Some(root_context),
470 spans: Vec::new(),
471 console_timestamps: Vec::new(),
472 service_name: service_name.into(),
473 }
474 }
475
476 pub fn start_span(
478 &mut self,
479 name: impl Into<String>,
480 category: impl Into<String>,
481 ) -> TraceSpan {
482 let context = self
483 .root_context
484 .as_ref()
485 .map(TraceContext::child)
486 .unwrap_or_default();
487
488 TraceSpan::with_context(name, category, context)
489 }
490
491 pub fn record_span(&mut self, span: TraceSpan) {
493 self.spans.push(span);
494 }
495
496 pub fn record_console(&mut self, message: impl Into<String>) {
498 self.console_timestamps.push((now_micros(), message.into()));
499 }
500
501 #[must_use]
503 pub fn traceparent(&self) -> Option<String> {
504 self.root_context.as_ref().map(TraceContext::to_traceparent)
505 }
506
507 #[must_use]
509 pub fn to_chrome_trace(&self) -> ChromeTrace {
510 let mut trace = ChromeTrace::new();
511 let pid = 1;
512
513 for (tid, span) in self.spans.iter().enumerate() {
515 trace.add_span(span, pid, tid as u32);
516 }
517
518 for (ts, msg) in &self.console_timestamps {
520 trace.add_instant(msg.clone(), "console", *ts, pid, 0);
521 }
522
523 let metadata = serde_json::json!({
525 "service_name": self.service_name,
526 "trace_id": self.root_context.as_ref().map(|c| &c.trace_id),
527 });
528 trace.set_metadata(metadata);
529
530 trace
531 }
532}
533
534fn generate_trace_id() -> String {
540 use std::collections::hash_map::RandomState;
541 use std::hash::{BuildHasher, Hasher};
542
543 let hasher = RandomState::new();
544 let mut h1 = hasher.build_hasher();
545 let mut h2 = hasher.build_hasher();
546
547 h1.write_u64(now_micros());
548 h2.write_u64(std::process::id() as u64);
549
550 format!("{:016x}{:016x}", h1.finish(), h2.finish())
551}
552
553fn generate_span_id() -> String {
555 use std::collections::hash_map::RandomState;
556 use std::hash::{BuildHasher, Hasher};
557
558 let hasher = RandomState::new();
559 let mut h = hasher.build_hasher();
560 h.write_u64(now_micros());
561 h.write_u32(std::process::id());
562
563 format!("{:016x}", h.finish())
564}
565
566fn now_micros() -> u64 {
568 SystemTime::now()
569 .duration_since(UNIX_EPOCH)
570 .map(|d| d.as_micros() as u64)
571 .unwrap_or(0)
572}
573
574#[cfg(test)]
579#[allow(clippy::unwrap_used, clippy::expect_used, clippy::float_cmp)]
580mod tests {
581 use super::*;
582
583 mod trace_context_tests {
584 use super::*;
585
586 #[test]
587 fn test_new() {
588 let ctx = TraceContext::new();
589 assert_eq!(ctx.trace_id.len(), 32);
590 assert_eq!(ctx.parent_id.len(), 16);
591 assert!(ctx.is_sampled());
592 }
593
594 #[test]
595 fn test_parse_valid() {
596 let traceparent = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
597 let ctx = TraceContext::parse(traceparent).unwrap();
598 assert_eq!(ctx.trace_id, "0af7651916cd43dd8448eb211c80319c");
599 assert_eq!(ctx.parent_id, "b7ad6b7169203331");
600 assert_eq!(ctx.flags, 1);
601 }
602
603 #[test]
604 fn test_parse_invalid_parts() {
605 assert!(TraceContext::parse("00-abc").is_none());
606 }
607
608 #[test]
609 fn test_parse_invalid_version() {
610 let traceparent = "01-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
611 assert!(TraceContext::parse(traceparent).is_none());
612 }
613
614 #[test]
615 fn test_parse_invalid_trace_id_length() {
616 let traceparent = "00-abc-b7ad6b7169203331-01";
617 assert!(TraceContext::parse(traceparent).is_none());
618 }
619
620 #[test]
621 fn test_to_traceparent() {
622 let ctx = TraceContext {
623 trace_id: "0af7651916cd43dd8448eb211c80319c".to_string(),
624 parent_id: "b7ad6b7169203331".to_string(),
625 flags: 1,
626 };
627 assert_eq!(
628 ctx.to_traceparent(),
629 "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
630 );
631 }
632
633 #[test]
634 fn test_child() {
635 let ctx = TraceContext::new();
636 let child = ctx.child();
637 assert_eq!(child.trace_id, ctx.trace_id);
638 assert_ne!(child.parent_id, ctx.parent_id);
639 }
640
641 #[test]
642 fn test_display() {
643 let ctx = TraceContext::new();
644 let s = format!("{ctx}");
645 assert!(s.starts_with("00-"));
646 }
647 }
648
649 mod tracing_config_tests {
650 use super::*;
651
652 #[test]
653 fn test_default() {
654 let config = TracingConfig::default();
655 assert!(config.enabled);
656 assert_eq!(config.sample_rate, 1.0);
657 assert!(config.capture_console);
658 }
659
660 #[test]
661 fn test_disabled() {
662 let config = TracingConfig::disabled();
663 assert!(!config.enabled);
664 assert_eq!(config.sample_rate, 0.0);
665 }
666
667 #[test]
668 fn test_builder() {
669 let config = TracingConfig::new("my-service")
670 .with_sample_rate(0.5)
671 .with_console_capture(false);
672 assert_eq!(config.service_name, "my-service");
673 assert!((config.sample_rate - 0.5).abs() < f64::EPSILON);
674 assert!(!config.capture_console);
675 }
676
677 #[test]
678 fn test_sample_rate_clamped() {
679 let config = TracingConfig::default().with_sample_rate(2.0);
680 assert!((config.sample_rate - 1.0).abs() < f64::EPSILON);
681
682 let config = TracingConfig::default().with_sample_rate(-1.0);
683 assert!((config.sample_rate - 0.0).abs() < f64::EPSILON);
684 }
685 }
686
687 mod trace_span_tests {
688 use super::*;
689
690 #[test]
691 fn test_new() {
692 let span = TraceSpan::new("test-span", "test");
693 assert_eq!(span.name, "test-span");
694 assert_eq!(span.category, "test");
695 assert!(span.end_us.is_none());
696 }
697
698 #[test]
699 fn test_add_attribute() {
700 let mut span = TraceSpan::new("test", "test");
701 span.add_attribute("key", "value");
702 assert_eq!(span.attributes.len(), 1);
703 assert_eq!(span.attributes[0], ("key".to_string(), "value".to_string()));
704 }
705
706 #[test]
707 fn test_end() {
708 let mut span = TraceSpan::new("test", "test");
709 std::thread::sleep(std::time::Duration::from_millis(1));
710 span.end();
711 assert!(span.end_us.is_some());
712 assert!(span.duration_us().unwrap() > 0);
713 }
714
715 #[test]
716 fn test_duration_before_end() {
717 let span = TraceSpan::new("test", "test");
718 assert!(span.duration_us().is_none());
719 }
720 }
721
722 mod chrome_trace_tests {
723 use super::*;
724
725 #[test]
726 fn test_new() {
727 let trace = ChromeTrace::new();
728 assert!(trace.trace_events.is_empty());
729 assert!(trace.metadata.is_none());
730 }
731
732 #[test]
733 fn test_add_span() {
734 let mut trace = ChromeTrace::new();
735 let mut span = TraceSpan::new("test-span", "test");
736 span.end();
737 trace.add_span(&span, 1, 1);
738 assert_eq!(trace.trace_events.len(), 1);
739 assert_eq!(trace.trace_events[0].name, "test-span");
740 }
741
742 #[test]
743 fn test_add_instant() {
744 let mut trace = ChromeTrace::new();
745 trace.add_instant("instant-event", "test", 1000, 1, 1);
746 assert_eq!(trace.trace_events.len(), 1);
747 assert_eq!(trace.trace_events[0].ph, "I");
748 }
749
750 #[test]
751 fn test_to_json() {
752 let mut trace = ChromeTrace::new();
753 trace.add_instant("test", "test", 0, 1, 1);
754 let json = trace.to_json().unwrap();
755 assert!(json.contains("traceEvents"));
756 assert!(json.contains("test"));
757 }
758
759 #[test]
760 fn test_nested_spans() {
761 let mut trace = ChromeTrace::new();
762 let mut parent = TraceSpan::new("parent", "test");
763 let mut child = TraceSpan::new("child", "test");
764 child.end();
765 parent.add_child(child);
766 parent.end();
767 trace.add_span(&parent, 1, 1);
768 assert_eq!(trace.trace_events.len(), 2);
769 }
770 }
771
772 mod trace_collector_tests {
773 use super::*;
774
775 #[test]
776 fn test_new() {
777 let collector = TraceCollector::new("test-service");
778 assert!(collector.root_context.is_some());
779 assert_eq!(collector.service_name, "test-service");
780 }
781
782 #[test]
783 fn test_start_span() {
784 let mut collector = TraceCollector::new("test");
785 let span = collector.start_span("test-span", "browser");
786 assert_eq!(span.name, "test-span");
787 assert_eq!(span.category, "browser");
788 }
789
790 #[test]
791 fn test_record_span() {
792 let mut collector = TraceCollector::new("test");
793 let mut span = collector.start_span("test", "test");
794 span.end();
795 collector.record_span(span);
796 assert_eq!(collector.spans.len(), 1);
797 }
798
799 #[test]
800 fn test_record_console() {
801 let mut collector = TraceCollector::new("test");
802 collector.record_console("test message");
803 assert_eq!(collector.console_timestamps.len(), 1);
804 }
805
806 #[test]
807 fn test_traceparent() {
808 let collector = TraceCollector::new("test");
809 let traceparent = collector.traceparent().unwrap();
810 assert!(traceparent.starts_with("00-"));
811 }
812
813 #[test]
814 fn test_to_chrome_trace() {
815 let mut collector = TraceCollector::new("test-service");
816 let mut span = collector.start_span("test-span", "browser");
817 span.end();
818 collector.record_span(span);
819 collector.record_console("console message");
820
821 let chrome_trace = collector.to_chrome_trace();
822 assert_eq!(chrome_trace.trace_events.len(), 2); }
824 }
825}