@@ -5,11 +5,13 @@ use serde::{Deserialize, Serialize};
55use thiserror:: Error ;
66use uuid:: Uuid ;
77
8+ use super :: {
9+ feedback:: { Feedback , FeedbackEvent } ,
10+ v7 as protocol,
11+ } ;
812use crate :: Dsn ;
913use crate :: { protocol:: v7:: ClientReport , utils:: ts_rfc3339_opt} ;
1014
11- use super :: v7 as protocol;
12-
1315use protocol:: {
1416 Attachment , AttachmentType , ClientSdkInfo , DynamicSamplingContext , Event , Log , Metric ,
1517 MonitorCheckIn , SessionAggregates , SessionUpdate , Transaction ,
@@ -135,6 +137,9 @@ enum EnvelopeItemType {
135137 /// A client report.
136138 #[ serde( rename = "client_report" ) ]
137139 ClientReport ,
140+ /// A User Feedback Item type.
141+ #[ serde( rename = "feedback" ) ]
142+ Feedback ,
138143}
139144
140145/// An Envelope Item Header.
@@ -188,6 +193,13 @@ pub enum EnvelopeItem {
188193 ClientReport ( ClientReport ) ,
189194 /// A container for a list of multiple items.
190195 ItemContainer ( ItemContainer ) ,
196+ /// A User Feedback item.
197+ ///
198+ /// Feedback is transmitted as an [`Event`] carrying a `feedback` context, wrapped in a
199+ /// [`FeedbackEvent`] which guarantees that context is present. Construct it via
200+ /// `EnvelopeItem::from(feedback)`, and use [`EnvelopeItem::as_feedback`] to recover the
201+ /// feedback.
202+ Feedback ( FeedbackEvent ) ,
191203 /// This is a sentinel item used to `filter` raw envelopes.
192204 Raw ,
193205 // TODO:
@@ -276,9 +288,21 @@ impl EnvelopeItem {
276288 Self :: MonitorCheckIn ( _) => Some ( EnvelopeItemType :: MonitorCheckIn ) ,
277289 Self :: ClientReport ( _) => Some ( EnvelopeItemType :: ClientReport ) ,
278290 Self :: ItemContainer ( container) => Some ( container. item_type ( ) ) ,
291+ Self :: Feedback ( _) => Some ( EnvelopeItemType :: Feedback ) ,
279292 Self :: Raw => None ,
280293 }
281294 }
295+
296+ /// Returns the [`Feedback`] carried by this item.
297+ ///
298+ /// Returns `None` for any item that is not a feedback item, or whose wrapped event is missing
299+ /// its `feedback` context.
300+ pub fn as_feedback ( & self ) -> Option < & Feedback > {
301+ match self {
302+ Self :: Feedback ( feedback) => Some ( feedback. feedback ( ) ) ,
303+ _ => None ,
304+ }
305+ }
282306}
283307
284308impl From < Event < ' static > > for EnvelopeItem {
@@ -341,6 +365,18 @@ impl From<ClientReport> for EnvelopeItem {
341365 }
342366}
343367
368+ impl From < Feedback > for EnvelopeItem {
369+ fn from ( feedback : Feedback ) -> Self {
370+ EnvelopeItem :: Feedback ( feedback. into ( ) )
371+ }
372+ }
373+
374+ impl From < FeedbackEvent > for EnvelopeItem {
375+ fn from ( feedback : FeedbackEvent ) -> Self {
376+ EnvelopeItem :: Feedback ( feedback)
377+ }
378+ }
379+
344380/// An Iterator over the items of an Envelope.
345381#[ derive( Clone ) ]
346382pub struct EnvelopeItemIter < ' s > {
@@ -457,10 +493,19 @@ impl Envelope {
457493 } ;
458494
459495 if self . headers . event_id . is_none ( ) {
460- if let EnvelopeItem :: Event ( ref event) = item {
461- self . headers . event_id = Some ( event. event_id ) ;
462- } else if let EnvelopeItem :: Transaction ( ref transaction) = item {
463- self . headers . event_id = Some ( transaction. event_id ) ;
496+ match item {
497+ EnvelopeItem :: Event ( ref event) => {
498+ self . headers . event_id = Some ( event. event_id ) ;
499+ }
500+ // Feedback wraps an event with its own id, so it sets the envelope `event_id` too;
501+ // otherwise `filter` would drop attachments from a feedback-only envelope.
502+ EnvelopeItem :: Feedback ( ref feedback) => {
503+ self . headers . event_id = Some ( feedback. event ( ) . event_id ) ;
504+ }
505+ EnvelopeItem :: Transaction ( ref transaction) => {
506+ self . headers . event_id = Some ( transaction. event_id ) ;
507+ }
508+ _ => { }
464509 }
465510 }
466511 items. push ( item) ;
@@ -626,6 +671,7 @@ impl Envelope {
626671 serde_json:: to_writer ( & mut item_buf, & wrapper) ?
627672 }
628673 } ,
674+ EnvelopeItem :: Feedback ( feedback) => serde_json:: to_writer ( & mut item_buf, feedback) ?,
629675 EnvelopeItem :: Raw => {
630676 continue ;
631677 }
@@ -823,6 +869,11 @@ impl Envelope {
823869 serde_json:: from_slice :: < ItemsSerdeWrapper < _ > > ( payload)
824870 . map ( |x| EnvelopeItem :: ItemContainer ( ItemContainer :: Metrics ( x. items . into ( ) ) ) )
825871 }
872+ EnvelopeItemType :: Feedback => {
873+ // `FeedbackEvent`'s `Deserialize` rejects an event missing its feedback context,
874+ // so a plain event mislabeled as feedback cannot be silently accepted here.
875+ serde_json:: from_slice ( payload) . map ( EnvelopeItem :: Feedback )
876+ }
826877 }
827878 . map_err ( EnvelopeError :: InvalidItemPayload ) ?;
828879
@@ -983,6 +1034,113 @@ mod test {
9831034 )
9841035 }
9851036
1037+ #[ test]
1038+ fn test_feedback ( ) {
1039+ let feedback = Feedback :: new ( "It broke." )
1040+ . with_contact_email ( "john.doe@example.com" )
1041+ . with_name ( "John Doe" ) ;
1042+ // `FeedbackEvent::from` fills in a random event id and the current timestamp, so
1043+ // overwrite them here to keep the serialized output deterministic.
1044+ let mut feedback = FeedbackEvent :: from ( feedback) ;
1045+ feedback. event_mut ( ) . event_id =
1046+ Uuid :: parse_str ( "22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c" ) . unwrap ( ) ;
1047+ feedback. event_mut ( ) . timestamp = timestamp ( "2020-07-20T14:51:14.296Z" ) ;
1048+
1049+ let mut envelope = Envelope :: new ( ) ;
1050+ envelope. add_item ( EnvelopeItem :: Feedback ( feedback) ) ;
1051+ assert_eq ! (
1052+ to_str( envelope) ,
1053+ r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
1054+ {"type":"feedback","length":212}
1055+ {"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","level":"info","timestamp":1595256674.296,"contexts":{"feedback":{"type":"feedback","contact_email":"john.doe@example.com","name":"John Doe","message":"It broke."}}}
1056+ "#
1057+ )
1058+ }
1059+
1060+ #[ test]
1061+ fn test_feedback_omits_empty_optional_fields ( ) {
1062+ let feedback = Feedback :: new ( "It broke." ) ;
1063+ // `FeedbackEvent::from` fills in a random event id and the current timestamp, so
1064+ // overwrite them here to keep the serialized output deterministic.
1065+ let mut feedback = FeedbackEvent :: from ( feedback) ;
1066+ feedback. event_mut ( ) . event_id =
1067+ Uuid :: parse_str ( "22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c" ) . unwrap ( ) ;
1068+ feedback. event_mut ( ) . timestamp = timestamp ( "2020-07-20T14:51:14.296Z" ) ;
1069+
1070+ let mut envelope = Envelope :: new ( ) ;
1071+ envelope. add_item ( EnvelopeItem :: Feedback ( feedback) ) ;
1072+ // The absent optional fields are omitted rather than serialized as `null`.
1073+ let serialized = to_str ( envelope) ;
1074+ assert_eq ! (
1075+ serialized,
1076+ r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
1077+ {"type":"feedback","length":155}
1078+ {"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","level":"info","timestamp":1595256674.296,"contexts":{"feedback":{"type":"feedback","message":"It broke."}}}
1079+ "#
1080+ ) ;
1081+
1082+ // The item round-trips back into a feedback item, and the feedback is recoverable.
1083+ let deserialized = Envelope :: from_slice ( serialized. as_bytes ( ) ) . unwrap ( ) ;
1084+ let item = deserialized. items ( ) . next ( ) . unwrap ( ) ;
1085+ assert ! ( matches!( item, EnvelopeItem :: Feedback ( _) ) ) ;
1086+ let recovered = item. as_feedback ( ) . unwrap ( ) ;
1087+ assert_eq ! ( recovered. message, "It broke." ) ;
1088+ assert_eq ! ( recovered. contact_email, None ) ;
1089+ assert_eq ! ( recovered. name, None ) ;
1090+ }
1091+
1092+ #[ test]
1093+ fn test_feedback_without_context_is_rejected ( ) {
1094+ // A `feedback`-typed item whose payload is a plain event with no feedback context must be
1095+ // rejected rather than silently accepted as feedback.
1096+ let bytes = b"\
1097+ {}\n \
1098+ {\" type\" :\" feedback\" }\n \
1099+ {\" event_id\" :\" 22d00b3fd1b14b5d8d2049d138cd8a9c\" }\n \
1100+ ";
1101+
1102+ let err = Envelope :: from_slice ( bytes) . unwrap_err ( ) ;
1103+ assert ! ( matches!( err, EnvelopeError :: InvalidItemPayload ( _) ) ) ;
1104+ }
1105+
1106+ #[ test]
1107+ fn test_feedback_context_type_inferred ( ) {
1108+ // A feedback context without an explicit `type` is inferred from its `feedback` key, so the
1109+ // item deserializes as feedback rather than being rejected.
1110+ let bytes = b"\
1111+ {}\n \
1112+ {\" type\" :\" feedback\" }\n \
1113+ {\" event_id\" :\" 22d00b3fd1b14b5d8d2049d138cd8a9c\" ,\" contexts\" :{\" feedback\" :{\" message\" :\" It broke.\" }}}\n \
1114+ ";
1115+
1116+ let envelope = Envelope :: from_slice ( bytes) . unwrap ( ) ;
1117+ let item = envelope. items ( ) . next ( ) . unwrap ( ) ;
1118+ assert_eq ! ( item. as_feedback( ) . unwrap( ) . message, "It broke." ) ;
1119+ }
1120+
1121+ #[ test]
1122+ fn test_feedback_sets_envelope_event_id ( ) {
1123+ let event_id = Uuid :: parse_str ( "22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c" ) . unwrap ( ) ;
1124+ let mut feedback = FeedbackEvent :: from ( Feedback :: new ( "It broke." ) ) ;
1125+ feedback. event_mut ( ) . event_id = event_id;
1126+
1127+ let mut envelope = Envelope :: new ( ) ;
1128+ envelope. add_item ( EnvelopeItem :: Feedback ( feedback) ) ;
1129+ envelope. add_item ( Attachment {
1130+ buffer : b"screenshot" . to_vec ( ) ,
1131+ filename : "screenshot.png" . to_owned ( ) ,
1132+ ..Default :: default ( )
1133+ } ) ;
1134+
1135+ // The feedback item populates the envelope `event_id`.
1136+ assert_eq ! ( envelope. uuid( ) , Some ( & event_id) ) ;
1137+
1138+ // Because the envelope has an `event_id`, `filter` keeps the feedback's attachment instead
1139+ // of dropping it as an orphan.
1140+ let filtered = envelope. filter ( |_item : & EnvelopeItem | true ) . unwrap ( ) ;
1141+ assert_eq ! ( filtered. items( ) . count( ) , 2 ) ;
1142+ }
1143+
9861144 #[ test]
9871145 fn test_session ( ) {
9881146 let session_id = Uuid :: parse_str ( "22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c" ) . unwrap ( ) ;
@@ -1465,13 +1623,24 @@ some content
14651623 } ]
14661624 . into ( ) ;
14671625
1626+ // Feedback
1627+ let mut feedback = FeedbackEvent :: from (
1628+ Feedback :: new ( "It broke." )
1629+ . with_contact_email ( "john.doe@example.com" )
1630+ . with_name ( "John Doe" ) ,
1631+ ) ;
1632+ // Pin the timestamp so the `SystemTime -> f64 -> SystemTime` round-trip is stable; the
1633+ // sub-second precision of `SystemTime::now()` does not survive it.
1634+ feedback. event_mut ( ) . timestamp = timestamp ( "2020-07-20T14:51:14.296Z" ) ;
1635+
14681636 let mut envelope: Envelope = Envelope :: new ( ) ;
14691637 envelope. add_item ( event) ;
14701638 envelope. add_item ( transaction) ;
14711639 envelope. add_item ( session) ;
14721640 envelope. add_item ( attachment) ;
14731641 envelope. add_item ( logs) ;
14741642 envelope. add_item ( metrics) ;
1643+ envelope. add_item ( EnvelopeItem :: Feedback ( feedback) ) ;
14751644
14761645 let serialized = to_str ( envelope) ;
14771646 let deserialized = Envelope :: from_slice ( serialized. as_bytes ( ) ) . unwrap ( ) ;
@@ -1682,6 +1851,13 @@ some content
16821851 ) ;
16831852 }
16841853
1854+ #[ test]
1855+ fn losses_on_drop_maps_feedback_to_feedback ( ) {
1856+ let envelope: Envelope = Feedback :: new ( "It broke." ) . into ( ) ;
1857+
1858+ assert_eq ! ( collect_losses( & envelope) , vec![ ( Category :: Feedback , 1 ) ] ) ;
1859+ }
1860+
16851861 #[ test]
16861862 fn losses_on_drop_skips_client_reports ( ) {
16871863 let envelope: Envelope = ClientReport :: new ( <[ Item ; 0 ] >:: default ( ) ) . into ( ) ;
@@ -1723,6 +1899,7 @@ some content
17231899 unit: None ,
17241900 attributes: Map :: new( ) ,
17251901 } ] ) ;
1902+ envelope. add_item ( Feedback :: new ( "flattened feedback" ) ) ;
17261903
17271904 assert_eq ! (
17281905 collect_losses( & envelope) ,
@@ -1734,6 +1911,7 @@ some content
17341911 ( Category :: LogByte , 9 ) ,
17351912 ( Category :: TraceMetric , 1 ) ,
17361913 ( Category :: TraceMetricByte , 24 ) ,
1914+ ( Category :: Feedback , 1 ) ,
17371915 ]
17381916 ) ;
17391917 }
0 commit comments