rovenue/events/
envelope.rs

1// =============================================================
2// EventEnvelope — wire format for SDK-ingested events
3// =============================================================
4//
5// Serialises to / deserialises from the camelCase JSON shape that
6// the server's /v1/events endpoint consumes.  All optional fields
7// are omitted when absent so the wire payload stays minimal.
8
9use super::IdentityContext;
10use serde::{Deserialize, Serialize};
11
12/// Funnel-attribution payload for a `funnel_*` event.
13///
14/// Carries its own `screen_id`: without it a funnel is a single event and
15/// an eight-screen onboarding is indistinguishable from a one-screen
16/// funnel — there is no drop-off to see.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct FunnelContext {
20    pub funnel_id: String,
21    /// The presentation id (`open_funnel` mints one per open, W10) — the
22    /// server requires it (W6) and the shaped wire carries it as the
23    /// ClickHouse `sessionId`.
24    pub session_id: String,
25    pub placement_id: String,
26    pub placement_revision: i64,
27    /// The published version the device is running.
28    pub revision: i64,
29    /// Which screen this event is about. Absent on funnel-level events
30    /// (`funnel_view`, `funnel_complete`, `funnel_dismiss`).
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub screen_id: Option<String>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub variant_id: Option<String>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub experiment_key: Option<String>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct EventEnvelope {
42    /// Wire format version (EVENT_WIRE_VERSION). Populated by `track()`.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub version: Option<u8>,
45
46    /// Stable, client-generated id reused across retries so downstream
47    /// fan-out can dedupe. Populated by `track()` when the caller omits it.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub event_id: Option<String>,
50
51    pub event_type: String,
52
53    /// ISO-8601 UTC timestamp, e.g. "2026-05-28T10:00:00Z"
54    pub occurred_at: String,
55
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub subscriber_id: Option<String>,
58
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub product_id: Option<String>,
61
62    /// Decimal string, e.g. "9.99"
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub amount: Option<String>,
65
66    /// ISO-4217 three-letter code, e.g. "USD"
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub currency: Option<String>,
69
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub event_source_url: Option<String>,
72
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub identity_context: Option<IdentityContext>,
75
76    /// `funnel_*` attribution payload. Absent for every other event type.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub funnel_context: Option<FunnelContext>,
79}