1use crate::events::Event;
24use crate::mls_observability::MlsLifecycleEvent;
25use crate::telemetry::device::DeviceCapabilitySnapshot;
26use crate::telemetry::metrics_snapshot::MetricsFrame;
27use crate::telemetry::routing::RoutingDecision;
28use crate::telemetry::transport_state::TransportStateEvent;
29
30#[non_exhaustive]
35#[derive(Debug, Clone)]
36pub enum TelemetryRecord {
37 Protocol(Box<Event>),
44 Mls(MlsLifecycleEvent),
46 MetricsSnapshot(Box<MetricsFrame>),
51 TransportState(TransportStateEvent),
54 Routing(Box<RoutingDecision>),
59 Device(DeviceCapabilitySnapshot),
61}
62
63impl TelemetryRecord {
64 pub fn name(&self) -> &'static str {
67 match self {
68 TelemetryRecord::Protocol(event) => event.telemetry_name(),
69 TelemetryRecord::Mls(event) => event.telemetry_name(),
70 TelemetryRecord::MetricsSnapshot(frame) => frame.telemetry_name(),
71 TelemetryRecord::TransportState(event) => event.telemetry_name(),
72 TelemetryRecord::Routing(decision) => decision.telemetry_name(),
73 TelemetryRecord::Device(snapshot) => snapshot.telemetry_name(),
74 }
75 }
76}
77
78#[cfg(test)]
79pub(crate) mod tests {
80 use super::*;
81 use crate::events::{
82 DorsEscalationPhase, DorsEscalationReasonCode, DorsReasonCode, PresenceSource,
83 PresenceStatus, SecurityWarningCode, WelcomeReasonCode,
84 };
85 use crate::mls_observability::{DecryptionFailureKind, MlsOperationContext};
86 use std::collections::{HashMap, HashSet};
87
88 pub(crate) const ALL_TELEMETRY_NAMES: &[&str] = &[
109 "protocol.message.sent",
111 "protocol.message.received",
112 "protocol.message.delivered",
113 "protocol.message.failed",
114 "protocol.message.decryption_failed",
115 "protocol.transport.switched",
116 "protocol.relay.promoted",
117 "protocol.relay.demoted",
118 "protocol.neighbor.discovered",
119 "protocol.neighbor.lost",
120 "protocol.identity.ready",
121 "protocol.network.metrics",
122 "protocol.file.progress",
123 "protocol.file.received",
124 "protocol.file.receive_failed",
125 "protocol.media.sent",
126 "protocol.media.send_failed",
127 "protocol.message.deferred",
128 "protocol.message.retrying",
129 "protocol.message.undeliverable",
130 "protocol.media.resend_required",
131 "protocol.ack.evicted",
132 "protocol.fragment.assembly_evicted",
133 "protocol.relay.demoted_battery",
134 "protocol.secure_session.established",
135 "protocol.secure_session.failed",
136 "protocol.convergence.diag",
137 "protocol.welcome.send_attempted",
138 "protocol.welcome.send_succeeded",
139 "protocol.welcome.send_failed",
140 "protocol.welcome.send_expired",
141 "protocol.connection.request_received",
142 "protocol.connection.request_undeliverable",
143 "protocol.connection.accepted",
144 "protocol.connection.rejected",
145 "protocol.connection.request_cancelled",
146 "protocol.group.created",
147 "protocol.group.message_received",
148 "protocol.group.member_added",
149 "protocol.group.member_removed",
150 "protocol.group.info",
151 "protocol.group.user_groups",
152 "protocol.group.error",
153 "protocol.group.relay_sync_changed",
154 "protocol.username.resolved",
155 "protocol.group.message_sent",
156 "protocol.group.message_partial_failure",
157 "protocol.group.delivery_report",
158 "protocol.group.rich_extras_dropped",
159 "protocol.group.unauthorized_membership_change",
160 "protocol.group.epoch_fork_detected",
161 "protocol.group.epoch_fork_resolved",
162 "protocol.group.role_changed",
163 "protocol.group.renamed",
164 "protocol.service.discovered",
165 "protocol.service.request_received",
166 "protocol.service.response_received",
167 "protocol.presence.updated",
168 "protocol.typing.received",
169 "protocol.read_receipt.received",
170 "protocol.dors.score_updated",
171 "protocol.dors.transport_selected",
172 "protocol.dors.transport_switched",
173 "protocol.dors.escalation_triggered",
174 "protocol.security.warning",
175 "protocol.message.relayed",
176 "protocol.user.blocked",
177 "protocol.user.unblocked",
178 "protocol.data.changed",
179 "protocol.data.doc_removed",
180 "protocol.data.doc_size_warning",
181 "protocol.data.attachment_requested",
182 "protocol.data.attachment_received",
183 "protocol.data.attachment_unavailable",
184 "protocol.data.doc_unsyncable",
185 "mls.initialized",
187 "mls.encryption_used",
188 "mls.decryption_failed",
189 "mls.session_missing",
190 "mls.session_establishing",
191 "mls.session_ready",
192 "protocol.metrics.snapshot",
201 "protocol.transport.state_changed",
202 "protocol.routing.decision",
203 "protocol.device.capability_changed",
204 ];
205
206 fn is_well_formed(name: &str) -> bool {
209 if name.is_empty() {
210 return false;
211 }
212 let mut segments = name.split('.');
213 let mut segment_count = 0;
214 for segment in &mut segments {
215 if segment.is_empty() {
216 return false;
217 }
218 if !segment
219 .chars()
220 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
221 {
222 return false;
223 }
224 segment_count += 1;
225 }
226 segment_count >= 2
227 }
228
229 #[test]
236 fn telemetry_record_is_smaller_than_unboxed_event() {
237 use std::mem::size_of;
238 assert!(
239 size_of::<TelemetryRecord>() < size_of::<Event>(),
240 "TelemetryRecord ({} bytes) is not smaller than Event ({} bytes) \
241 — the `Box<Event>` variant was removed or the size budget was \
242 blown by another variant. Re-box the Protocol variant or audit \
243 the new variant's payload.",
244 size_of::<TelemetryRecord>(),
245 size_of::<Event>(),
246 );
247 }
248
249 #[test]
250 fn protocol_variant_names_are_dotted() {
251 let event = Event::MessageReceived {
252 message_id: "m".into(),
253 sender: "s".into(),
254 recipient: "r".into(),
255 content: String::new(),
256 reply_context: None,
257 hop_count: 0,
258 transport: "ble".into(),
259 timestamp: 0,
260 lamport_clock: 0,
261 reply_to_msg: None,
262 content_type: String::new(),
263 media_metadata: None,
264 forward_info: None,
265 encrypted: false,
266 app_id: String::new(),
267 };
268 assert_eq!(
269 TelemetryRecord::Protocol(Box::new(event)).name(),
270 "protocol.message.received",
271 );
272 }
273
274 #[test]
275 fn mls_variant_names_are_dotted() {
276 let event = MlsLifecycleEvent::Initialized {
277 timestamp_ms: 0,
278 session_id: "s".into(),
279 group_id: None,
280 peer_id: None,
281 context: MlsOperationContext::Initialize,
282 error_category: None,
283 };
284 assert_eq!(TelemetryRecord::Mls(event).name(), "mls.initialized");
285 }
286
287 #[allow(dead_code)]
296 fn event_variant_exhaustiveness_ward(e: &Event) {
297 match e {
298 Event::MessageSent { .. }
299 | Event::MessageReceived { .. }
300 | Event::MessageDelivered { .. }
301 | Event::MessageFailed { .. }
302 | Event::MessageDecryptionFailed { .. }
303 | Event::TransportSwitched { .. }
304 | Event::RelayPromoted { .. }
305 | Event::RelayDemoted { .. }
306 | Event::NeighborDiscovered { .. }
307 | Event::NeighborLost { .. }
308 | Event::IdentityReady { .. }
309 | Event::NetworkMetrics { .. }
310 | Event::FileProgress { .. }
311 | Event::FileReceived { .. }
312 | Event::FileReceiveFailed { .. }
313 | Event::MediaSent { .. }
314 | Event::MediaSendFailed { .. }
315 | Event::MessageDeferred { .. }
316 | Event::MessageRetrying { .. }
317 | Event::MessageUndeliverable { .. }
318 | Event::MediaResendRequired { .. }
319 | Event::AckEvicted { .. }
320 | Event::FragmentAssemblyEvicted { .. }
321 | Event::RelayDemotedBattery { .. }
322 | Event::SecureSessionEstablished { .. }
323 | Event::SecureSessionFailed { .. }
324 | Event::ConvergenceDiag { .. }
325 | Event::WelcomeSendAttempted { .. }
326 | Event::WelcomeSendSucceeded { .. }
327 | Event::WelcomeSendFailed { .. }
328 | Event::WelcomeSendExpired { .. }
329 | Event::ConnectionRequestReceived { .. }
330 | Event::ConnectionRequestUndeliverable { .. }
331 | Event::ConnectionAccepted { .. }
332 | Event::ConnectionRejected { .. }
333 | Event::ConnectionRequestCancelled { .. }
334 | Event::GroupCreated { .. }
335 | Event::GroupMessageReceived { .. }
336 | Event::GroupMemberAdded { .. }
337 | Event::GroupMemberRemoved { .. }
338 | Event::GroupInfo { .. }
339 | Event::UserGroups { .. }
340 | Event::GroupError { .. }
341 | Event::GroupRelaySyncChanged { .. }
342 | Event::UsernameResolved { .. }
343 | Event::GroupMessageSent { .. }
344 | Event::GroupMessagePartialFailure { .. }
345 | Event::GroupMessageDeliveryReport { .. }
346 | Event::GroupRichExtrasDropped { .. }
347 | Event::GroupUnauthorizedMembershipChange { .. }
348 | Event::GroupEpochForkDetected { .. }
349 | Event::GroupEpochForkResolved { .. }
350 | Event::GroupRoleChanged { .. }
351 | Event::GroupRenamed { .. }
352 | Event::ServiceDiscovered { .. }
353 | Event::ServiceRequestReceived { .. }
354 | Event::ServiceResponseReceived { .. }
355 | Event::PresenceUpdated { .. }
356 | Event::TypingIndicatorReceived { .. }
357 | Event::ReadReceiptReceived { .. }
358 | Event::DorsScoreUpdated { .. }
359 | Event::DorsTransportSelected { .. }
360 | Event::DorsTransportSwitched { .. }
361 | Event::DorsEscalationTriggered { .. }
362 | Event::SecurityWarning { .. }
363 | Event::MessageRelayed { .. }
364 | Event::UserBlocked { .. }
365 | Event::UserUnblocked { .. }
366 | Event::DataChanged { .. }
367 | Event::DataDocRemoved { .. }
368 | Event::DataDocSizeWarning { .. }
369 | Event::DataAttachmentRequested { .. }
370 | Event::DataAttachmentReceived { .. }
371 | Event::DataAttachmentUnavailable { .. }
372 | Event::DataDocUnsyncable { .. } => (),
373 }
374 }
375
376 #[allow(dead_code)]
379 fn mls_lifecycle_exhaustiveness_ward(e: &MlsLifecycleEvent) {
380 match e {
381 MlsLifecycleEvent::Initialized { .. }
382 | MlsLifecycleEvent::EncryptionUsed { .. }
383 | MlsLifecycleEvent::DecryptionFailed { .. }
384 | MlsLifecycleEvent::SessionMissing { .. }
385 | MlsLifecycleEvent::SessionEstablishing { .. }
386 | MlsLifecycleEvent::SessionReady { .. } => (),
387 }
388 }
389
390 pub(crate) fn event_exemplars() -> Vec<Event> {
394 vec![
395 Event::MessageSent {
396 message_id: String::new(),
397 sender: String::new(),
398 recipient: String::new(),
399 content: String::new(),
400 priority: String::new(),
401 requires_ack: false,
402 timestamp: 0,
403 lamport_clock: 0,
404 forward_info: None,
405 },
406 Event::MessageReceived {
407 message_id: String::new(),
408 sender: String::new(),
409 recipient: String::new(),
410 content: String::new(),
411 hop_count: 0,
412 transport: String::new(),
413 timestamp: 0,
414 lamport_clock: 0,
415 reply_to_msg: None,
416 reply_context: None,
417 content_type: String::new(),
418 media_metadata: None,
419 forward_info: None,
420 encrypted: false,
421 app_id: String::new(),
422 },
423 Event::MessageDelivered {
424 message_id: String::new(),
425 latency_ms: 0,
426 hop_count: 0,
427 transport: String::new(),
428 },
429 Event::MessageFailed {
430 message_id: String::new(),
431 reason: String::new(),
432 retry_count: 0,
433 },
434 Event::MessageDecryptionFailed {
435 message_id: String::new(),
436 sender: String::new(),
437 code: crate::events::DecryptionFailureCode::Unknown,
438 reason: String::new(),
439 sealed_at_ms: None,
440 session_established_at_ms: None,
441 },
442 Event::TransportSwitched {
443 from: None,
444 to: String::new(),
445 reason: String::new(),
446 },
447 Event::RelayPromoted {
448 connection_count: 0,
449 battery_level: None,
450 },
451 Event::RelayDemoted {
452 reason: String::new(),
453 },
454 Event::NeighborDiscovered {
455 peer_id: String::new(),
456 transport: String::new(),
457 rssi: None,
458 },
459 Event::NeighborLost {
460 peer_id: String::new(),
461 },
462 Event::IdentityReady {
463 address: String::new(),
464 },
465 Event::NetworkMetrics {
466 neighbor_count: 0,
467 relay_count: 0,
468 delivery_ratio: 0.0,
469 avg_latency_ms: 0,
470 },
471 Event::FileProgress {
472 file_id: String::new(),
473 chunks_sent: 0,
474 total_chunks: 0,
475 percentage: 0,
476 },
477 Event::FileReceived {
478 file_id: String::new(),
479 file_name: String::new(),
480 file_size: 0,
481 sender: String::new(),
482 content_type: String::new(),
483 media_metadata: None,
484 file_data: String::new(),
485 timestamp: None,
486 caption: None,
487 reply_to_msg: None,
488 reply_context: None,
489 forward_info: None,
490 app_id: None,
491 },
492 Event::FileReceiveFailed {
493 file_id: String::new(),
494 file_name: String::new(),
495 sender: String::new(),
496 reason: String::new(),
497 },
498 Event::MediaSent {
499 file_id: String::new(),
500 content_type: String::new(),
501 recipient: String::new(),
502 },
503 Event::MediaSendFailed {
504 file_id: String::new(),
505 recipient: String::new(),
506 reason: String::new(),
507 },
508 Event::MessageDeferred {
509 message_id: String::new(),
510 recipient: String::new(),
511 reason: String::new(),
512 retry_count: 0,
513 next_retry_at: None,
514 },
515 Event::MessageRetrying {
516 message_id: String::new(),
517 recipient: String::new(),
518 retry_count: 0,
519 next_retry_at: 0,
520 },
521 Event::MessageUndeliverable {
522 message_id: String::new(),
523 recipient: String::new(),
524 reason: String::new(),
525 file_id: None,
526 },
527 Event::MediaResendRequired {
528 file_id: String::new(),
529 recipient: String::new(),
530 file_name: String::new(),
531 file_size: 0,
532 app_id: None,
533 },
534 Event::AckEvicted {
535 message_id: String::new(),
536 priority: String::new(),
537 reason: String::new(),
538 },
539 Event::FragmentAssemblyEvicted {
540 message_id: String::new(),
541 completion_percent: 0,
542 reason: String::new(),
543 },
544 Event::RelayDemotedBattery {
545 battery_level: 0,
546 min_required: 0,
547 },
548 Event::SecureSessionEstablished {
549 peer_id: String::new(),
550 group_id: String::new(),
551 is_session: false,
552 initiated_by_local: false,
553 },
554 Event::SecureSessionFailed {
555 peer_id: String::new(),
556 reason: String::new(),
557 },
558 Event::ConvergenceDiag {
559 stage: String::new(),
560 peer_id: String::new(),
561 detail: String::new(),
562 },
563 Event::WelcomeSendAttempted {
564 peer_id: String::new(),
565 message_id: String::new(),
566 group_id: String::new(),
567 attempt: 0,
568 },
569 Event::WelcomeSendSucceeded {
570 peer_id: String::new(),
571 message_id: String::new(),
572 group_id: String::new(),
573 attempt: 0,
574 },
575 Event::WelcomeSendFailed {
576 peer_id: String::new(),
577 message_id: String::new(),
578 group_id: String::new(),
579 attempt: 0,
580 reason_code: WelcomeReasonCode::InternalError,
581 transport_error: None,
582 retryable: false,
583 next_retry_at: None,
584 },
585 Event::WelcomeSendExpired {
586 peer_id: String::new(),
587 message_id: String::new(),
588 attempt: 0,
589 reason_code: WelcomeReasonCode::RetryExhausted,
590 },
591 Event::ConnectionRequestReceived {
592 sender: String::new(),
593 sender_name: String::new(),
594 timestamp: 0,
595 key_package: None,
596 initial_message: None,
597 },
598 Event::ConnectionRequestUndeliverable {
599 recipient: String::new(),
600 message_id: String::new(),
601 reason: String::new(),
602 },
603 Event::ConnectionAccepted {
604 accepted_by: String::new(),
605 accepted_by_name: String::new(),
606 timestamp: 0,
607 key_package: None,
608 },
609 Event::ConnectionRejected {
610 rejected_by: String::new(),
611 },
612 Event::ConnectionRequestCancelled {
613 cancelled_by: String::new(),
614 },
615 Event::GroupCreated {
616 group_id: String::new(),
617 name: String::new(),
618 },
619 Event::GroupMessageReceived {
620 group_id: String::new(),
621 sender: String::new(),
622 content: String::new(),
623 timestamp: String::new(),
624 message_id: String::new(),
625 reply_to_msg: None,
626 forward_info: None,
627 media_metadata: None,
628 content_type: None,
629 },
630 Event::GroupMemberAdded {
631 group_id: String::new(),
632 user_id: String::new(),
633 added_by: String::new(),
634 group_name: None,
635 authorized: Some(true),
636 },
637 Event::GroupMemberRemoved {
638 group_id: String::new(),
639 user_id: String::new(),
640 removed_by: String::new(),
641 authorized: Some(true),
642 },
643 Event::GroupInfo {
644 group_id: String::new(),
645 name: String::new(),
646 created_by: String::new(),
647 created_at: String::new(),
648 members: Vec::new(),
649 },
650 Event::UserGroups { groups: Vec::new() },
651 Event::GroupError {
652 reason: String::new(),
653 group_id: None,
654 },
655 Event::GroupRelaySyncChanged {
656 group_id: String::new(),
657 synced: false,
658 reason: String::new(),
659 },
660 Event::UsernameResolved {
661 username: String::new(),
662 claims: Vec::new(),
663 rejected: 0,
664 truncated: 0,
665 },
666 Event::GroupMessageSent {
667 group_id: String::new(),
668 message_ids: Vec::new(),
669 member_count: 0,
670 },
671 Event::GroupMessagePartialFailure {
672 group_id: String::new(),
673 failed_members: Vec::new(),
674 succeeded_members: Vec::new(),
675 },
676 Event::GroupMessageDeliveryReport {
677 group_id: String::new(),
678 message_id: String::new(),
679 delivered: Vec::new(),
680 pushed: Vec::new(),
681 missed_reissued: Vec::new(),
682 },
683 Event::GroupRichExtrasDropped {
684 group_id: String::new(),
685 unknown_members: Vec::new(),
686 },
687 Event::GroupUnauthorizedMembershipChange {
688 group_id: String::new(),
689 committer: String::new(),
690 added: Vec::new(),
691 removed: Vec::new(),
692 reason: String::new(),
693 enforced: false,
694 },
695 Event::GroupEpochForkDetected {
696 group_id: String::new(),
697 local_epoch: None,
698 },
699 Event::GroupEpochForkResolved {
700 group_id: String::new(),
701 resolved_epoch: 0,
702 failed_members: Vec::new(),
703 },
704 Event::GroupRoleChanged {
705 group_id: String::new(),
706 user_id: String::new(),
707 new_role: String::new(),
708 changed_by: String::new(),
709 },
710 Event::GroupRenamed {
711 group_id: String::new(),
712 new_name: String::new(),
713 old_name: None,
714 renamed_by: String::new(),
715 },
716 Event::ServiceDiscovered {
717 query_id: String::new(),
718 service_id: String::new(),
719 version: String::new(),
720 provider_peer_id: String::new(),
721 capabilities: HashMap::new(),
722 hop_count: 0,
723 },
724 Event::ServiceRequestReceived {
725 request_id: String::new(),
726 service_id: String::new(),
727 method: String::new(),
728 body: String::new(),
729 sender: String::new(),
730 encrypted: false,
731 },
732 Event::ServiceResponseReceived {
733 request_id: String::new(),
734 service_id: String::new(),
735 status: String::new(),
736 body: String::new(),
737 provider_peer_id: String::new(),
738 encrypted: false,
739 },
740 Event::PresenceUpdated {
741 peer_id: String::new(),
742 status: PresenceStatus::Online,
743 timestamp: 0,
744 last_seen_ms: None,
745 source: PresenceSource::Internet,
746 },
747 Event::TypingIndicatorReceived {
748 sender: String::new(),
749 conversation_id: String::new(),
750 is_typing: false,
751 timestamp: 0,
752 },
753 Event::ReadReceiptReceived {
754 sender: String::new(),
755 message_ids: Vec::new(),
756 timestamp: 0,
757 },
758 Event::DorsScoreUpdated { scores: Vec::new() },
759 Event::DorsTransportSelected {
760 from: None,
761 transport: String::new(),
762 reason_code: DorsReasonCode::InitialSelection,
763 score: 0.0,
764 },
765 Event::DorsTransportSwitched {
766 from: None,
767 to: String::new(),
768 reason_code: DorsReasonCode::PrimarySelected,
769 reason_detail: None,
770 },
771 Event::DorsEscalationTriggered {
772 phase: DorsEscalationPhase::Triggered,
773 from: String::new(),
774 to: String::new(),
775 reason_code: DorsEscalationReasonCode::FallbackSuccess,
776 reason_detail: None,
777 },
778 Event::SecurityWarning {
779 peer_id: String::new(),
780 reason_code: SecurityWarningCode::SenderAddressMismatch,
781 reason: String::new(),
782 },
783 Event::MessageRelayed {
784 message_id: String::new(),
785 sender: String::new(),
786 recipient: String::new(),
787 hop_count: 0,
788 remaining_ttl: 0,
789 },
790 Event::UserBlocked {
791 user_id: String::new(),
792 },
793 Event::UserUnblocked {
794 user_id: String::new(),
795 },
796 Event::DataChanged {
797 space_id: String::new(),
798 doc_id: String::new(),
799 delta_bytes: 0,
800 },
801 Event::DataDocRemoved {
802 space_id: String::new(),
803 doc_id: String::new(),
804 by: crate::events::DocRemovedBy::Local,
805 },
806 Event::DataDocSizeWarning {
807 space_id: String::new(),
808 doc_id: String::new(),
809 compacted_bytes: 0,
810 cap_bytes: 0,
811 },
812 Event::DataAttachmentRequested {
813 space_id: String::new(),
814 peer_id: String::new(),
815 hash: String::new(),
816 },
817 Event::DataAttachmentReceived {
818 space_id: String::new(),
819 peer_id: String::new(),
820 hash: String::new(),
821 data: String::new(),
822 },
823 Event::DataAttachmentUnavailable {
824 space_id: String::new(),
825 peer_id: String::new(),
826 hash: String::new(),
827 reason: String::new(),
828 },
829 Event::DataDocUnsyncable {
830 space_id: String::new(),
831 doc_id: String::new(),
832 bytes: 0,
833 reason: String::new(),
834 },
835 ]
836 }
837
838 fn mls_lifecycle_exemplars() -> Vec<MlsLifecycleEvent> {
841 vec![
842 MlsLifecycleEvent::Initialized {
843 timestamp_ms: 0,
844 session_id: String::new(),
845 group_id: None,
846 peer_id: None,
847 context: MlsOperationContext::Initialize,
848 error_category: None,
849 },
850 MlsLifecycleEvent::EncryptionUsed {
851 timestamp_ms: 0,
852 session_id: String::new(),
853 group_id: None,
854 peer_id: None,
855 context: MlsOperationContext::Send,
856 error_category: None,
857 },
858 MlsLifecycleEvent::DecryptionFailed {
859 timestamp_ms: 0,
860 session_id: String::new(),
861 group_id: None,
862 peer_id: None,
863 context: MlsOperationContext::Receive,
864 error_category: None,
865 failure_kind: DecryptionFailureKind::Unknown,
866 },
867 MlsLifecycleEvent::SessionMissing {
868 timestamp_ms: 0,
869 session_id: String::new(),
870 group_id: None,
871 peer_id: None,
872 context: MlsOperationContext::SessionLookup,
873 error_category: None,
874 },
875 MlsLifecycleEvent::SessionEstablishing {
876 timestamp_ms: 0,
877 session_id: String::new(),
878 group_id: None,
879 peer_id: None,
880 context: MlsOperationContext::Welcome,
881 error_category: None,
882 },
883 MlsLifecycleEvent::SessionReady {
884 timestamp_ms: 0,
885 session_id: String::new(),
886 group_id: None,
887 peer_id: None,
888 context: MlsOperationContext::Welcome,
889 error_category: None,
890 },
891 ]
892 }
893
894 fn simple_record_names() -> Vec<&'static str> {
898 use crate::telemetry::device::DeviceCapabilitySnapshot;
899 use crate::telemetry::metrics_snapshot::MetricsFrame;
900 use crate::telemetry::routing::RoutingDecision;
901 use crate::telemetry::transport_state::TransportStateEvent;
902 vec![
903 MetricsFrame::NAME,
904 TransportStateEvent::NAME,
905 RoutingDecision::NAME,
906 DeviceCapabilitySnapshot::NAME,
907 ]
908 }
909
910 #[test]
918 fn impl_telemetry_names_are_unique() {
919 let mut names: Vec<&'static str> = Vec::new();
920 for e in event_exemplars() {
921 names.push(e.telemetry_name());
922 }
923 for e in mls_lifecycle_exemplars() {
924 names.push(e.telemetry_name());
925 }
926 names.extend(simple_record_names());
927 let unique: HashSet<&&'static str> = names.iter().collect();
928 assert_eq!(
929 unique.len(),
930 names.len(),
931 "two variants produced the same telemetry_name() — distinct \
932 events would silently merge at the sink. Names: {names:?}",
933 );
934 }
935
936 #[test]
943 fn impl_telemetry_names_match_catalogue() {
944 let mut impl_names: Vec<&'static str> = Vec::new();
945 for e in event_exemplars() {
946 impl_names.push(e.telemetry_name());
947 }
948 for e in mls_lifecycle_exemplars() {
949 impl_names.push(e.telemetry_name());
950 }
951 impl_names.extend(simple_record_names());
952
953 let impl_set: HashSet<&&'static str> = impl_names.iter().collect();
954 let catalogue_set: HashSet<&&str> = ALL_TELEMETRY_NAMES.iter().collect();
955
956 let missing_from_catalogue: Vec<&&'static str> = impl_set
957 .iter()
958 .copied()
959 .filter(|n| !ALL_TELEMETRY_NAMES.contains(*n))
960 .collect();
961 assert!(
962 missing_from_catalogue.is_empty(),
963 "implementation produces names not in ALL_TELEMETRY_NAMES: \
964 {missing_from_catalogue:?}. Update the catalogue.",
965 );
966
967 let stale_in_catalogue: Vec<&&str> = catalogue_set
968 .iter()
969 .copied()
970 .filter(|n| !impl_names.iter().any(|produced| produced == *n))
971 .collect();
972 assert!(
973 stale_in_catalogue.is_empty(),
974 "catalogue carries names the implementation does not produce: \
975 {stale_in_catalogue:?}. Remove from the catalogue or restore \
976 the match arm.",
977 );
978
979 assert_eq!(
980 impl_names.len(),
981 ALL_TELEMETRY_NAMES.len(),
982 "catalogue length ({}) differs from implementation variant \
983 count ({})",
984 ALL_TELEMETRY_NAMES.len(),
985 impl_names.len(),
986 );
987 }
988
989 #[test]
995 fn catalogue_entries_are_unique() {
996 let set: HashSet<&&str> = ALL_TELEMETRY_NAMES.iter().collect();
997 assert_eq!(
998 set.len(),
999 ALL_TELEMETRY_NAMES.len(),
1000 "duplicate entry in ALL_TELEMETRY_NAMES",
1001 );
1002 }
1003
1004 #[test]
1005 fn all_telemetry_names_match_grammar() {
1006 for name in ALL_TELEMETRY_NAMES {
1007 assert!(
1008 is_well_formed(name),
1009 "telemetry name '{name}' does not match snake.dot.case grammar",
1010 );
1011 }
1012 }
1013
1014 #[test]
1015 fn grammar_checker_rejects_malformed_names() {
1016 assert!(!is_well_formed(""));
1017 assert!(!is_well_formed("flat"));
1018 assert!(!is_well_formed(".leading"));
1019 assert!(!is_well_formed("trailing."));
1020 assert!(!is_well_formed("double..dot"));
1021 assert!(!is_well_formed("Upper.Case"));
1022 assert!(!is_well_formed("has-dash.segment"));
1023 assert!(is_well_formed("ok.name"));
1024 assert!(is_well_formed("a.b.c"));
1025 assert!(is_well_formed("with_underscore.segment"));
1026 assert!(is_well_formed("has_digit1.seg2"));
1027 }
1028}