From b9864a931c8a21edb7f8f158ee63015324ea7441 Mon Sep 17 00:00:00 2001 From: Zach Mitchell Date: Mon, 17 Feb 2025 20:26:32 -0700 Subject: [PATCH] feat: add feedback api --- sentry-types/src/protocol/envelope.rs | 19 +++++++++++-- sentry-types/src/protocol/feedback.rs | 39 +++++++++++++++++++++++++++ sentry-types/src/protocol/mod.rs | 1 + sentry-types/src/protocol/v7.rs | 9 +++++++ 4 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 sentry-types/src/protocol/feedback.rs diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 33807d359..60fd6650d 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -4,11 +4,10 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; +use super::{feedback::Feedback, v7 as protocol}; use crate::utils::ts_rfc3339_opt; use crate::Dsn; -use super::v7 as protocol; - use protocol::{ Attachment, AttachmentType, ClientSdkInfo, DynamicSamplingContext, Event, Log, MonitorCheckIn, SessionAggregates, SessionUpdate, Transaction, @@ -127,6 +126,9 @@ enum EnvelopeItemType { /// A container of Log items. #[serde(rename = "log")] LogsContainer, + /// A User Feedback Item type. + #[serde(rename = "feedback")] + Feedback, } /// An Envelope Item Header. @@ -178,6 +180,8 @@ pub enum EnvelopeItem { MonitorCheckIn(MonitorCheckIn), /// A container for a list of multiple items. ItemContainer(ItemContainer), + /// A User Feedback item. + Feedback(Event<'static>), /// This is a sentinel item used to `filter` raw envelopes. Raw, // TODO: @@ -283,6 +287,12 @@ impl From> for EnvelopeItem { } } +impl From for EnvelopeItem { + fn from(feedback: Feedback) -> Self { + EnvelopeItem::Feedback(feedback.to_new_event()) + } +} + /// An Iterator over the items of an Envelope. #[derive(Clone)] pub struct EnvelopeItemIter<'s> { @@ -507,6 +517,7 @@ impl Envelope { serde_json::to_writer(&mut item_buf, &wrapper)? } }, + EnvelopeItem::Feedback(feedback) => serde_json::to_writer(&mut item_buf, feedback)?, EnvelopeItem::Raw => { continue; } @@ -518,6 +529,7 @@ impl Envelope { EnvelopeItem::Transaction(_) => "transaction", EnvelopeItem::MonitorCheckIn(_) => "check_in", EnvelopeItem::ItemContainer(container) => container.ty(), + EnvelopeItem::Feedback(_) => "feedback", EnvelopeItem::Attachment(_) | EnvelopeItem::Raw => unreachable!(), }; @@ -677,6 +689,9 @@ impl Envelope { serde_json::from_slice::>(payload) .map(|x| EnvelopeItem::ItemContainer(ItemContainer::Logs(x.items.into()))) } + EnvelopeItemType::Feedback => { + serde_json::from_slice(payload).map(EnvelopeItem::Feedback) + } } .map_err(EnvelopeError::InvalidItemPayload)?; diff --git a/sentry-types/src/protocol/feedback.rs b/sentry-types/src/protocol/feedback.rs new file mode 100644 index 000000000..cf31b1647 --- /dev/null +++ b/sentry-types/src/protocol/feedback.rs @@ -0,0 +1,39 @@ +use std::{collections::BTreeMap, time::SystemTime}; + +use serde::{Deserialize, Serialize}; + +use crate::random_uuid; + +use super::v7::{Context, Event, Level}; + +/// Represents feedback from a user. +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] +pub struct Feedback { + /// The user's contact email + pub contact_email: Option, + /// The user's name + pub name: Option, + /// The feedback from the user + pub message: String, +} + +impl Feedback { + pub(crate) fn to_context(&self) -> Context { + Context::Feedback(Box::new(self.clone())) + } + + pub(crate) fn to_new_event(&self) -> Event<'static> { + let map = { + let mut map = BTreeMap::new(); + map.insert("feedback".to_string(), self.to_context()); + map + }; + Event { + event_id: random_uuid(), + level: Level::Info, + timestamp: SystemTime::now(), + contexts: map, + ..Default::default() + } + } +} diff --git a/sentry-types/src/protocol/mod.rs b/sentry-types/src/protocol/mod.rs index bf0d8977d..9ba1f1c05 100644 --- a/sentry-types/src/protocol/mod.rs +++ b/sentry-types/src/protocol/mod.rs @@ -15,5 +15,6 @@ pub use v7 as latest; mod attachment; mod envelope; +mod feedback; mod monitor; mod session; diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index b2f8ee99a..166c82387 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -26,6 +26,7 @@ use crate::utils::{display_from_str_opt, ts_rfc3339_opt, ts_seconds_float}; pub use super::attachment::*; pub use super::envelope::*; +pub use super::feedback::*; pub use super::monitor::*; pub use super::session::*; @@ -1102,6 +1103,8 @@ pub enum Context { Otel(Box), /// HTTP response data. Response(Box), + /// User feedback + Feedback(Box), /// Generic other context data. #[serde(rename = "unknown")] Other(Map), @@ -1120,6 +1123,7 @@ impl Context { Context::Gpu(..) => "gpu", Context::Otel(..) => "otel", Context::Response(..) => "response", + Context::Feedback(..) => "feedback", Context::Other(..) => "unknown", } } @@ -1721,6 +1725,9 @@ pub struct Event<'a> { /// SDK metadata #[serde(default, skip_serializing_if = "Option::is_none")] pub sdk: Option>, + /// The event type + #[serde(default, skip_serializing_if = "Option::is_none")] + pub r#type: Option, } impl Default for Event<'_> { @@ -1753,6 +1760,7 @@ impl Default for Event<'_> { extra: Default::default(), debug_meta: Default::default(), sdk: Default::default(), + r#type: Default::default(), } } } @@ -1798,6 +1806,7 @@ impl<'a> Event<'a> { extra: self.extra, debug_meta: Cow::Owned(self.debug_meta.into_owned()), sdk: self.sdk.map(|x| Cow::Owned(x.into_owned())), + r#type: self.r#type, } } }