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 };
267 assert_eq!(
268 TelemetryRecord::Protocol(Box::new(event)).name(),
269 "protocol.message.received",
270 );
271 }
272
273 #[test]
274 fn mls_variant_names_are_dotted() {
275 let event = MlsLifecycleEvent::Initialized {
276 timestamp_ms: 0,
277 session_id: "s".into(),
278 group_id: None,
279 peer_id: None,
280 context: MlsOperationContext::Initialize,
281 error_category: None,
282 };
283 assert_eq!(TelemetryRecord::Mls(event).name(), "mls.initialized");
284 }
285
286 #[allow(dead_code)]
295 fn event_variant_exhaustiveness_ward(e: &Event) {
296 match e {
297 Event::MessageSent { .. }
298 | Event::MessageReceived { .. }
299 | Event::MessageDelivered { .. }
300 | Event::MessageFailed { .. }
301 | Event::MessageDecryptionFailed { .. }
302 | Event::TransportSwitched { .. }
303 | Event::RelayPromoted { .. }
304 | Event::RelayDemoted { .. }
305 | Event::NeighborDiscovered { .. }
306 | Event::NeighborLost { .. }
307 | Event::IdentityReady { .. }
308 | Event::NetworkMetrics { .. }
309 | Event::FileProgress { .. }
310 | Event::FileReceived { .. }
311 | Event::FileReceiveFailed { .. }
312 | Event::MediaSent { .. }
313 | Event::MediaSendFailed { .. }
314 | Event::MessageDeferred { .. }
315 | Event::MessageRetrying { .. }
316 | Event::MessageUndeliverable { .. }
317 | Event::MediaResendRequired { .. }
318 | Event::AckEvicted { .. }
319 | Event::FragmentAssemblyEvicted { .. }
320 | Event::RelayDemotedBattery { .. }
321 | Event::SecureSessionEstablished { .. }
322 | Event::SecureSessionFailed { .. }
323 | Event::ConvergenceDiag { .. }
324 | Event::WelcomeSendAttempted { .. }
325 | Event::WelcomeSendSucceeded { .. }
326 | Event::WelcomeSendFailed { .. }
327 | Event::WelcomeSendExpired { .. }
328 | Event::ConnectionRequestReceived { .. }
329 | Event::ConnectionRequestUndeliverable { .. }
330 | Event::ConnectionAccepted { .. }
331 | Event::ConnectionRejected { .. }
332 | Event::ConnectionRequestCancelled { .. }
333 | Event::GroupCreated { .. }
334 | Event::GroupMessageReceived { .. }
335 | Event::GroupMemberAdded { .. }
336 | Event::GroupMemberRemoved { .. }
337 | Event::GroupInfo { .. }
338 | Event::UserGroups { .. }
339 | Event::GroupError { .. }
340 | Event::GroupRelaySyncChanged { .. }
341 | Event::UsernameResolved { .. }
342 | Event::GroupMessageSent { .. }
343 | Event::GroupMessagePartialFailure { .. }
344 | Event::GroupMessageDeliveryReport { .. }
345 | Event::GroupRichExtrasDropped { .. }
346 | Event::GroupUnauthorizedMembershipChange { .. }
347 | Event::GroupEpochForkDetected { .. }
348 | Event::GroupEpochForkResolved { .. }
349 | Event::GroupRoleChanged { .. }
350 | Event::GroupRenamed { .. }
351 | Event::ServiceDiscovered { .. }
352 | Event::ServiceRequestReceived { .. }
353 | Event::ServiceResponseReceived { .. }
354 | Event::PresenceUpdated { .. }
355 | Event::TypingIndicatorReceived { .. }
356 | Event::ReadReceiptReceived { .. }
357 | Event::DorsScoreUpdated { .. }
358 | Event::DorsTransportSelected { .. }
359 | Event::DorsTransportSwitched { .. }
360 | Event::DorsEscalationTriggered { .. }
361 | Event::SecurityWarning { .. }
362 | Event::MessageRelayed { .. }
363 | Event::UserBlocked { .. }
364 | Event::UserUnblocked { .. }
365 | Event::DataChanged { .. }
366 | Event::DataDocRemoved { .. }
367 | Event::DataDocSizeWarning { .. }
368 | Event::DataAttachmentRequested { .. }
369 | Event::DataAttachmentReceived { .. }
370 | Event::DataAttachmentUnavailable { .. }
371 | Event::DataDocUnsyncable { .. } => (),
372 }
373 }
374
375 #[allow(dead_code)]
378 fn mls_lifecycle_exhaustiveness_ward(e: &MlsLifecycleEvent) {
379 match e {
380 MlsLifecycleEvent::Initialized { .. }
381 | MlsLifecycleEvent::EncryptionUsed { .. }
382 | MlsLifecycleEvent::DecryptionFailed { .. }
383 | MlsLifecycleEvent::SessionMissing { .. }
384 | MlsLifecycleEvent::SessionEstablishing { .. }
385 | MlsLifecycleEvent::SessionReady { .. } => (),
386 }
387 }
388
389 pub(crate) fn event_exemplars() -> Vec<Event> {
393 vec![
394 Event::MessageSent {
395 message_id: String::new(),
396 sender: String::new(),
397 recipient: String::new(),
398 content: String::new(),
399 priority: String::new(),
400 requires_ack: false,
401 timestamp: 0,
402 lamport_clock: 0,
403 forward_info: None,
404 },
405 Event::MessageReceived {
406 message_id: String::new(),
407 sender: String::new(),
408 recipient: String::new(),
409 content: String::new(),
410 hop_count: 0,
411 transport: String::new(),
412 timestamp: 0,
413 lamport_clock: 0,
414 reply_to_msg: None,
415 reply_context: None,
416 content_type: String::new(),
417 media_metadata: None,
418 forward_info: None,
419 encrypted: false,
420 },
421 Event::MessageDelivered {
422 message_id: String::new(),
423 latency_ms: 0,
424 hop_count: 0,
425 transport: String::new(),
426 },
427 Event::MessageFailed {
428 message_id: String::new(),
429 reason: String::new(),
430 retry_count: 0,
431 },
432 Event::MessageDecryptionFailed {
433 message_id: String::new(),
434 sender: String::new(),
435 code: crate::events::DecryptionFailureCode::Unknown,
436 reason: String::new(),
437 },
438 Event::TransportSwitched {
439 from: None,
440 to: String::new(),
441 reason: String::new(),
442 },
443 Event::RelayPromoted {
444 connection_count: 0,
445 battery_level: None,
446 },
447 Event::RelayDemoted {
448 reason: String::new(),
449 },
450 Event::NeighborDiscovered {
451 peer_id: String::new(),
452 transport: String::new(),
453 rssi: None,
454 },
455 Event::NeighborLost {
456 peer_id: String::new(),
457 },
458 Event::IdentityReady {
459 address: String::new(),
460 },
461 Event::NetworkMetrics {
462 neighbor_count: 0,
463 relay_count: 0,
464 delivery_ratio: 0.0,
465 avg_latency_ms: 0,
466 },
467 Event::FileProgress {
468 file_id: String::new(),
469 chunks_sent: 0,
470 total_chunks: 0,
471 percentage: 0,
472 },
473 Event::FileReceived {
474 file_id: String::new(),
475 file_name: String::new(),
476 file_size: 0,
477 sender: String::new(),
478 content_type: String::new(),
479 media_metadata: None,
480 file_data: String::new(),
481 timestamp: None,
482 caption: None,
483 reply_to_msg: None,
484 reply_context: None,
485 forward_info: None,
486 },
487 Event::FileReceiveFailed {
488 file_id: String::new(),
489 file_name: String::new(),
490 sender: String::new(),
491 reason: String::new(),
492 },
493 Event::MediaSent {
494 file_id: String::new(),
495 content_type: String::new(),
496 recipient: String::new(),
497 },
498 Event::MediaSendFailed {
499 file_id: String::new(),
500 recipient: String::new(),
501 reason: String::new(),
502 },
503 Event::MessageDeferred {
504 message_id: String::new(),
505 recipient: String::new(),
506 reason: String::new(),
507 retry_count: 0,
508 next_retry_at: None,
509 },
510 Event::MessageRetrying {
511 message_id: String::new(),
512 recipient: String::new(),
513 retry_count: 0,
514 next_retry_at: 0,
515 },
516 Event::MessageUndeliverable {
517 message_id: String::new(),
518 recipient: String::new(),
519 reason: String::new(),
520 file_id: None,
521 },
522 Event::MediaResendRequired {
523 file_id: String::new(),
524 recipient: String::new(),
525 file_name: String::new(),
526 file_size: 0,
527 },
528 Event::AckEvicted {
529 message_id: String::new(),
530 priority: String::new(),
531 reason: String::new(),
532 },
533 Event::FragmentAssemblyEvicted {
534 message_id: String::new(),
535 completion_percent: 0,
536 reason: String::new(),
537 },
538 Event::RelayDemotedBattery {
539 battery_level: 0,
540 min_required: 0,
541 },
542 Event::SecureSessionEstablished {
543 peer_id: String::new(),
544 group_id: String::new(),
545 is_session: false,
546 initiated_by_local: false,
547 },
548 Event::SecureSessionFailed {
549 peer_id: String::new(),
550 reason: String::new(),
551 },
552 Event::ConvergenceDiag {
553 stage: String::new(),
554 peer_id: String::new(),
555 detail: String::new(),
556 },
557 Event::WelcomeSendAttempted {
558 peer_id: String::new(),
559 message_id: String::new(),
560 group_id: String::new(),
561 attempt: 0,
562 },
563 Event::WelcomeSendSucceeded {
564 peer_id: String::new(),
565 message_id: String::new(),
566 group_id: String::new(),
567 attempt: 0,
568 },
569 Event::WelcomeSendFailed {
570 peer_id: String::new(),
571 message_id: String::new(),
572 group_id: String::new(),
573 attempt: 0,
574 reason_code: WelcomeReasonCode::InternalError,
575 transport_error: None,
576 retryable: false,
577 next_retry_at: None,
578 },
579 Event::WelcomeSendExpired {
580 peer_id: String::new(),
581 message_id: String::new(),
582 attempt: 0,
583 reason_code: WelcomeReasonCode::RetryExhausted,
584 },
585 Event::ConnectionRequestReceived {
586 sender: String::new(),
587 sender_name: String::new(),
588 timestamp: 0,
589 key_package: None,
590 initial_message: None,
591 },
592 Event::ConnectionRequestUndeliverable {
593 recipient: String::new(),
594 message_id: String::new(),
595 reason: String::new(),
596 },
597 Event::ConnectionAccepted {
598 accepted_by: String::new(),
599 accepted_by_name: String::new(),
600 timestamp: 0,
601 key_package: None,
602 },
603 Event::ConnectionRejected {
604 rejected_by: String::new(),
605 },
606 Event::ConnectionRequestCancelled {
607 cancelled_by: String::new(),
608 },
609 Event::GroupCreated {
610 group_id: String::new(),
611 name: String::new(),
612 },
613 Event::GroupMessageReceived {
614 group_id: String::new(),
615 sender: String::new(),
616 content: String::new(),
617 timestamp: String::new(),
618 message_id: String::new(),
619 reply_to_msg: None,
620 forward_info: None,
621 media_metadata: None,
622 content_type: None,
623 },
624 Event::GroupMemberAdded {
625 group_id: String::new(),
626 user_id: String::new(),
627 added_by: String::new(),
628 group_name: None,
629 authorized: Some(true),
630 },
631 Event::GroupMemberRemoved {
632 group_id: String::new(),
633 user_id: String::new(),
634 removed_by: String::new(),
635 authorized: Some(true),
636 },
637 Event::GroupInfo {
638 group_id: String::new(),
639 name: String::new(),
640 created_by: String::new(),
641 created_at: String::new(),
642 members: Vec::new(),
643 },
644 Event::UserGroups { groups: Vec::new() },
645 Event::GroupError {
646 reason: String::new(),
647 group_id: None,
648 },
649 Event::GroupRelaySyncChanged {
650 group_id: String::new(),
651 synced: false,
652 reason: String::new(),
653 },
654 Event::UsernameResolved {
655 username: String::new(),
656 claims: Vec::new(),
657 rejected: 0,
658 truncated: 0,
659 },
660 Event::GroupMessageSent {
661 group_id: String::new(),
662 message_ids: Vec::new(),
663 member_count: 0,
664 },
665 Event::GroupMessagePartialFailure {
666 group_id: String::new(),
667 failed_members: Vec::new(),
668 succeeded_members: Vec::new(),
669 },
670 Event::GroupMessageDeliveryReport {
671 group_id: String::new(),
672 message_id: String::new(),
673 delivered: Vec::new(),
674 pushed: Vec::new(),
675 missed_reissued: Vec::new(),
676 },
677 Event::GroupRichExtrasDropped {
678 group_id: String::new(),
679 unknown_members: Vec::new(),
680 },
681 Event::GroupUnauthorizedMembershipChange {
682 group_id: String::new(),
683 committer: String::new(),
684 added: Vec::new(),
685 removed: Vec::new(),
686 reason: String::new(),
687 enforced: false,
688 },
689 Event::GroupEpochForkDetected {
690 group_id: String::new(),
691 local_epoch: None,
692 },
693 Event::GroupEpochForkResolved {
694 group_id: String::new(),
695 resolved_epoch: 0,
696 failed_members: Vec::new(),
697 },
698 Event::GroupRoleChanged {
699 group_id: String::new(),
700 user_id: String::new(),
701 new_role: String::new(),
702 changed_by: String::new(),
703 },
704 Event::GroupRenamed {
705 group_id: String::new(),
706 new_name: String::new(),
707 old_name: None,
708 renamed_by: String::new(),
709 },
710 Event::ServiceDiscovered {
711 query_id: String::new(),
712 service_id: String::new(),
713 version: String::new(),
714 provider_peer_id: String::new(),
715 capabilities: HashMap::new(),
716 hop_count: 0,
717 },
718 Event::ServiceRequestReceived {
719 request_id: String::new(),
720 service_id: String::new(),
721 method: String::new(),
722 body: String::new(),
723 sender: String::new(),
724 },
725 Event::ServiceResponseReceived {
726 request_id: String::new(),
727 service_id: String::new(),
728 status: String::new(),
729 body: String::new(),
730 provider_peer_id: String::new(),
731 },
732 Event::PresenceUpdated {
733 peer_id: String::new(),
734 status: PresenceStatus::Online,
735 timestamp: 0,
736 last_seen_ms: None,
737 source: PresenceSource::Internet,
738 },
739 Event::TypingIndicatorReceived {
740 sender: String::new(),
741 conversation_id: String::new(),
742 is_typing: false,
743 timestamp: 0,
744 },
745 Event::ReadReceiptReceived {
746 sender: String::new(),
747 message_ids: Vec::new(),
748 timestamp: 0,
749 },
750 Event::DorsScoreUpdated { scores: Vec::new() },
751 Event::DorsTransportSelected {
752 from: None,
753 transport: String::new(),
754 reason_code: DorsReasonCode::InitialSelection,
755 score: 0.0,
756 },
757 Event::DorsTransportSwitched {
758 from: None,
759 to: String::new(),
760 reason_code: DorsReasonCode::PrimarySelected,
761 reason_detail: None,
762 },
763 Event::DorsEscalationTriggered {
764 phase: DorsEscalationPhase::Triggered,
765 from: String::new(),
766 to: String::new(),
767 reason_code: DorsEscalationReasonCode::FallbackSuccess,
768 reason_detail: None,
769 },
770 Event::SecurityWarning {
771 peer_id: String::new(),
772 reason_code: SecurityWarningCode::SenderAddressMismatch,
773 reason: String::new(),
774 },
775 Event::MessageRelayed {
776 message_id: String::new(),
777 sender: String::new(),
778 recipient: String::new(),
779 hop_count: 0,
780 remaining_ttl: 0,
781 },
782 Event::UserBlocked {
783 user_id: String::new(),
784 },
785 Event::UserUnblocked {
786 user_id: String::new(),
787 },
788 Event::DataChanged {
789 space_id: String::new(),
790 doc_id: String::new(),
791 delta_bytes: 0,
792 },
793 Event::DataDocRemoved {
794 space_id: String::new(),
795 doc_id: String::new(),
796 by: crate::events::DocRemovedBy::Local,
797 },
798 Event::DataDocSizeWarning {
799 space_id: String::new(),
800 doc_id: String::new(),
801 compacted_bytes: 0,
802 cap_bytes: 0,
803 },
804 Event::DataAttachmentRequested {
805 space_id: String::new(),
806 peer_id: String::new(),
807 hash: String::new(),
808 },
809 Event::DataAttachmentReceived {
810 space_id: String::new(),
811 peer_id: String::new(),
812 hash: String::new(),
813 data: String::new(),
814 },
815 Event::DataAttachmentUnavailable {
816 space_id: String::new(),
817 peer_id: String::new(),
818 hash: String::new(),
819 reason: String::new(),
820 },
821 Event::DataDocUnsyncable {
822 space_id: String::new(),
823 doc_id: String::new(),
824 bytes: 0,
825 reason: String::new(),
826 },
827 ]
828 }
829
830 fn mls_lifecycle_exemplars() -> Vec<MlsLifecycleEvent> {
833 vec![
834 MlsLifecycleEvent::Initialized {
835 timestamp_ms: 0,
836 session_id: String::new(),
837 group_id: None,
838 peer_id: None,
839 context: MlsOperationContext::Initialize,
840 error_category: None,
841 },
842 MlsLifecycleEvent::EncryptionUsed {
843 timestamp_ms: 0,
844 session_id: String::new(),
845 group_id: None,
846 peer_id: None,
847 context: MlsOperationContext::Send,
848 error_category: None,
849 },
850 MlsLifecycleEvent::DecryptionFailed {
851 timestamp_ms: 0,
852 session_id: String::new(),
853 group_id: None,
854 peer_id: None,
855 context: MlsOperationContext::Receive,
856 error_category: None,
857 failure_kind: DecryptionFailureKind::Unknown,
858 },
859 MlsLifecycleEvent::SessionMissing {
860 timestamp_ms: 0,
861 session_id: String::new(),
862 group_id: None,
863 peer_id: None,
864 context: MlsOperationContext::SessionLookup,
865 error_category: None,
866 },
867 MlsLifecycleEvent::SessionEstablishing {
868 timestamp_ms: 0,
869 session_id: String::new(),
870 group_id: None,
871 peer_id: None,
872 context: MlsOperationContext::Welcome,
873 error_category: None,
874 },
875 MlsLifecycleEvent::SessionReady {
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 ]
884 }
885
886 fn simple_record_names() -> Vec<&'static str> {
890 use crate::telemetry::device::DeviceCapabilitySnapshot;
891 use crate::telemetry::metrics_snapshot::MetricsFrame;
892 use crate::telemetry::routing::RoutingDecision;
893 use crate::telemetry::transport_state::TransportStateEvent;
894 vec![
895 MetricsFrame::NAME,
896 TransportStateEvent::NAME,
897 RoutingDecision::NAME,
898 DeviceCapabilitySnapshot::NAME,
899 ]
900 }
901
902 #[test]
910 fn impl_telemetry_names_are_unique() {
911 let mut names: Vec<&'static str> = Vec::new();
912 for e in event_exemplars() {
913 names.push(e.telemetry_name());
914 }
915 for e in mls_lifecycle_exemplars() {
916 names.push(e.telemetry_name());
917 }
918 names.extend(simple_record_names());
919 let unique: HashSet<&&'static str> = names.iter().collect();
920 assert_eq!(
921 unique.len(),
922 names.len(),
923 "two variants produced the same telemetry_name() — distinct \
924 events would silently merge at the sink. Names: {names:?}",
925 );
926 }
927
928 #[test]
935 fn impl_telemetry_names_match_catalogue() {
936 let mut impl_names: Vec<&'static str> = Vec::new();
937 for e in event_exemplars() {
938 impl_names.push(e.telemetry_name());
939 }
940 for e in mls_lifecycle_exemplars() {
941 impl_names.push(e.telemetry_name());
942 }
943 impl_names.extend(simple_record_names());
944
945 let impl_set: HashSet<&&'static str> = impl_names.iter().collect();
946 let catalogue_set: HashSet<&&str> = ALL_TELEMETRY_NAMES.iter().collect();
947
948 let missing_from_catalogue: Vec<&&'static str> = impl_set
949 .iter()
950 .copied()
951 .filter(|n| !ALL_TELEMETRY_NAMES.contains(*n))
952 .collect();
953 assert!(
954 missing_from_catalogue.is_empty(),
955 "implementation produces names not in ALL_TELEMETRY_NAMES: \
956 {missing_from_catalogue:?}. Update the catalogue.",
957 );
958
959 let stale_in_catalogue: Vec<&&str> = catalogue_set
960 .iter()
961 .copied()
962 .filter(|n| !impl_names.iter().any(|produced| produced == *n))
963 .collect();
964 assert!(
965 stale_in_catalogue.is_empty(),
966 "catalogue carries names the implementation does not produce: \
967 {stale_in_catalogue:?}. Remove from the catalogue or restore \
968 the match arm.",
969 );
970
971 assert_eq!(
972 impl_names.len(),
973 ALL_TELEMETRY_NAMES.len(),
974 "catalogue length ({}) differs from implementation variant \
975 count ({})",
976 ALL_TELEMETRY_NAMES.len(),
977 impl_names.len(),
978 );
979 }
980
981 #[test]
987 fn catalogue_entries_are_unique() {
988 let set: HashSet<&&str> = ALL_TELEMETRY_NAMES.iter().collect();
989 assert_eq!(
990 set.len(),
991 ALL_TELEMETRY_NAMES.len(),
992 "duplicate entry in ALL_TELEMETRY_NAMES",
993 );
994 }
995
996 #[test]
997 fn all_telemetry_names_match_grammar() {
998 for name in ALL_TELEMETRY_NAMES {
999 assert!(
1000 is_well_formed(name),
1001 "telemetry name '{name}' does not match snake.dot.case grammar",
1002 );
1003 }
1004 }
1005
1006 #[test]
1007 fn grammar_checker_rejects_malformed_names() {
1008 assert!(!is_well_formed(""));
1009 assert!(!is_well_formed("flat"));
1010 assert!(!is_well_formed(".leading"));
1011 assert!(!is_well_formed("trailing."));
1012 assert!(!is_well_formed("double..dot"));
1013 assert!(!is_well_formed("Upper.Case"));
1014 assert!(!is_well_formed("has-dash.segment"));
1015 assert!(is_well_formed("ok.name"));
1016 assert!(is_well_formed("a.b.c"));
1017 assert!(is_well_formed("with_underscore.segment"));
1018 assert!(is_well_formed("has_digit1.seg2"));
1019 }
1020}