Expand description
Event surface.
Two layers live here:
-
RawEvent— the M1 envelope: an opaque, type-tagged record. Thetypefield is the stable wire identity (see SDKALL_TELEMETRY_NAMESatrecord.rs:108-184, or one of our own derived names likemesh_metrics_rollupandmesh_session_summary);ts_msis the emit time;datais the per-type payload. The batch always deserializes intoRawEvents — they are cheap and never reject. -
The M3 typed layer (
Event+ the per-type*Datastructs +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 toEvent::Extensionand land in theraw_extension_eventscatchall (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_withon 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§
- Device
Capability Data protocol.device.capability_changed—{ battery_level?, is_charging, relay_role, changed_fields }.changed_fieldsis the SMALLINT bitmask (CHANGED_BATTERY=1, CHANGED_CHARGING=2, CHANGED_RELAY_ROLE=4).- Escalation
Reasons - Nested
escalation_reasonsobject onmesh_session_summary(plan §2.2). Flattened into theescalation_*columns server-side. - Message
Delivered Data protocol.message.delivered—{ latency_ms, hop_count, transport }.- Message
Relayed Data protocol.message.relayed—{ hop_count, remaining_ttl }.- Metrics
Rollup Data mesh_metrics_rollup(plan §2.1, Appendix A). Wire→column renames:retry_queue_critical_count_max→retry_queue_critical_max, thesends_*_sum/bytes_*_sumsuffixes are dropped, andtransport_time_msis flattened.bucket_start_msis on the wire but maps to the rowts(taken from the envelope’sts_ms), so it has no field here.- MlsDecryption
Failed Data mls.decryption_failed—{ failure_kind, context }. Wirecontextmaps to themls_contextcolumn. All IDs are stripped at the listener.- MlsSession
Missing Data mls.session_missing—{ context }. Wirecontextmaps tomls_context.- RawEvent
- Reason
Retry Data protocol.message.{failed,deferred}—{ reason_class, retry_count }. Both events share this shape (plan §2.3); thereason_classenum 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 freeTEXTwith no CHECK, so a client that grows a bucket needs no migration here.- Relay
Demoted Battery Data protocol.relay.demoted_battery—{ battery_level, min_required }. Emitted from the battery gate inevaluate_relay_role(SDK ≥ 0.11.0). Battery demotion is its own typed event, which is why it never reaches thereason_classclassifier.- Relay
Demoted Data protocol.relay.demoted—{ reason_class }. Emitted fromevaluate_relay_role(SDK ≥ 0.11.0): the configuration gate and theRelayActivity::Ceasedarm. Those two literals are the whole vocabulary — the anticipatedbattery_low/role_lost/shutdownwere never written (plan Appendix B).- Relay
Promoted Data protocol.relay.promoted—{ connection_count, battery_level }. Emitted from theRelayActivity::Beganarm ofevaluate_relay_role(SDK PR #110, shipped in 0.11.0 — the release that introduced telemetry, so no SDK the client supports lacks it).- Routing
Decision Data protocol.routing.decision—{ phase, from?, to?, winning_score?, reason_code? }. Wirefrom/tomap to thefrom_transport/to_transportcolumns; the optionalscores[]array (only present underrouting_diagnostic) has no column and is ignored.- Session
Summary Data mesh_session_summary(plan §2.2 + §2.4) — one row per session, emitted on app-background. The nestedtransport_time_ms/escalation_reasonsobjects are flattened into columns server-side;mls_decryption_failures_by_kindis stored as JSONB as-is.- Transport
State Data protocol.transport.state_changed—{ transport, previous, current }. Wireprevious/currentmap to theprevious_status/current_statuscolumns (per-transport Available/Unavailable status, not selection).- Transport
Time Ms - Nested
transport_time_msobject (plan §2.1 / §2.2). The server flattens these into the per-transport*_time_mscolumns.noneis a real bucket in the rollup (“no transport currently selected”) but has no column inraw_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