Skip to main content

Module event

Module event 

Source
Expand description

Event surface.

Two layers live here:

  • RawEvent — the M1 envelope: an opaque, type-tagged record. The type field is the stable wire identity (see SDK ALL_TELEMETRY_NAMES at record.rs:108-184, or one of our own derived names like mesh_metrics_rollup and mesh_session_summary); ts_ms is the emit time; data is the per-type payload. The batch always deserializes into RawEvents — they are cheap and never reject.

  • The M3 typed layer (Event + the per-type *Data structs + classify). After the envelope parses, the ingest server classifies each (type, &data) into a typed variant and routes it to its hypertable (plan §7.5). Unknown types fall through to Event::Extension and land in the raw_extension_events catchall (plan §2.6).

§Why the field types mirror the storage schema, not the wire spec

Each *Data struct types its fields to the column width of the matching raw_* hypertable (migration 0005_typed_raw_events.sql): SMALLINT→i16, INTEGER→i32, BIGINT→i64, REAL→f32. That is deliberate: a JSON number outside the column’s range then fails serde_json::from_value, so classify rejects the event (plan §7.5: “out-of-range drops the event”) instead of letting the value reach the bulk INSERT, where a single bad row would fail the whole batch. The plan’s §2.2/§2.3 u16/u32 hints are the wire’s logical ranges; the migration already widened them where a u16 could exceed i16 (see the §3.1 type-choice notes), and these structs follow the migration, not the hint.

§Negative values are accepted within the column range — by design

The raw_* columns are signed (SMALLINT/INTEGER/BIGINT) with no CHECK (col >= 0) constraint, even though most of these fields are logically unsigned (counts, durations, latencies, battery levels). So the boundary classify enforces is exactly the column’s storable range — [i16::MIN, i16::MAX] for SMALLINT, etc. — not the logical [0, …] range. A hop_count: -1 deserializes and is stored verbatim.

This is deliberate, not an oversight:

  • The raw tables are an unaggregated landing zone; the privacy guarantee is the 30-day retention drop, not field-level sanitisation. The M4 aggregation pass is the right place to clamp/discard nonsense (a WHERE col >= 0), where the rule lives in one query instead of being duplicated across ~40 nullable wire fields here.
  • Per-field non-negativity would need a custom deserialize_with on every numeric field — large surface area — and would reject the whole event for a single negative, losing its other (valid) fields. Storing a negative that an aggregation query can trivially filter is the better failure mode.
  • It keeps “passed classify” meaning precisely “fits the column”, which is the invariant the fan-out INSERT relies on: a classified event can never fail its bind. (Overflow past the column range is still rejected.)

The negative_value_within_column_range_is_accepted test pins this so the choice can’t silently flip.

All event-specific fields are Option<_>: the typed columns are nullable (migration 0005), the SDK listener emits a field only when present (its pick projection), and serde maps a missing Option field to None. The structs do not set deny_unknown_fields — an SDK that adds a field to a known event must not start failing validation (plan §2.6 forward-compat ethos); unknown keys within a known type are ignored.

Structs§

DeviceCapabilityData
protocol.device.capability_changed — { battery_level?, is_charging, relay_role, changed_fields }. changed_fields is the SMALLINT bitmask (CHANGED_BATTERY=1, CHANGED_CHARGING=2, CHANGED_RELAY_ROLE=4).
EscalationReasons
Nested escalation_reasons object on mesh_session_summary (plan §2.2). Flattened into the escalation_* columns server-side.
MessageDeliveredData
protocol.message.delivered — { latency_ms, hop_count, transport }.
MessageRelayedData
protocol.message.relayed — { hop_count, remaining_ttl }.
MetricsRollupData
mesh_metrics_rollup (plan §2.1, Appendix A). Wire→column renames: retry_queue_critical_count_max→retry_queue_critical_max, the sends_*_sum/bytes_*_sum suffixes are dropped, and transport_time_ms is flattened. bucket_start_ms is on the wire but maps to the row ts (taken from the envelope’s ts_ms), so it has no field here.
MlsDecryptionFailedData
mls.decryption_failed — { failure_kind, context }. Wire context maps to the mls_context column. All IDs are stripped at the listener.
MlsSessionMissingData
mls.session_missing — { context }. Wire context maps to mls_context.
RawEvent
ReasonRetryData
protocol.message.{failed,deferred} — { reason_class, retry_count }. Both events share this shape (plan §2.3); the reason_class enum is classified client-side (Appendix B — 21 buckets at SDK 0.24.1, the original twelve retained so older rows keep their meaning) and arrives as a string. The column is free TEXT with no CHECK, so a client that grows a bucket needs no migration here.
RelayDemotedBatteryData
protocol.relay.demoted_battery — { battery_level, min_required }. Emitted from the battery gate in evaluate_relay_role (SDK ≥ 0.11.0). Battery demotion is its own typed event, which is why it never reaches the reason_class classifier.
RelayDemotedData
protocol.relay.demoted — { reason_class }. Emitted from evaluate_relay_role (SDK ≥ 0.11.0): the configuration gate and the RelayActivity::Ceased arm. Those two literals are the whole vocabulary — the anticipated battery_low/role_lost/shutdown were never written (plan Appendix B).
RelayPromotedData
protocol.relay.promoted — { connection_count, battery_level }. Emitted from the RelayActivity::Began arm of evaluate_relay_role (SDK PR #110, shipped in 0.11.0 — the release that introduced telemetry, so no SDK the client supports lacks it).
RoutingDecisionData
protocol.routing.decision — { phase, from?, to?, winning_score?, reason_code? }. Wire from/to map to the from_transport/to_transport columns; the optional scores[] array (only present under routing_diagnostic) has no column and is ignored.
SessionSummaryData
mesh_session_summary (plan §2.2 + §2.4) — one row per session, emitted on app-background. The nested transport_time_ms/escalation_reasons objects are flattened into columns server-side; mls_decryption_failures_by_kind is stored as JSONB as-is.
TransportStateData
protocol.transport.state_changed — { transport, previous, current }. Wire previous/current map to the previous_status/current_status columns (per-transport Available/Unavailable status, not selection).
TransportTimeMs
Nested transport_time_ms object (plan §2.1 / §2.2). The server flattens these into the per-transport *_time_ms columns. none is a real bucket in the rollup (“no transport currently selected”) but has no column in raw_session_summary, so the session-summary insert ignores it.

Enums§

Event
A classified, schema-validated event ready to route to its hypertable.

Constants§

TYPE_DEVICE_CAPABILITY
TYPE_MESSAGE_DEFERRED
TYPE_MESSAGE_DELIVERED
TYPE_MESSAGE_FAILED
TYPE_MESSAGE_RELAYED
TYPE_METRICS_ROLLUP
TYPE_MLS_DECRYPTION_FAILED
TYPE_MLS_SESSION_MISSING
TYPE_RELAY_DEMOTED
TYPE_RELAY_DEMOTED_BATTERY
TYPE_RELAY_PROMOTED
TYPE_ROUTING_DECISION
TYPE_SESSION_SUMMARY
TYPE_TRANSPORT_STATE

Functions§

classify
Classify a wire (type, data) pair into a typed Event.