diff --git a/CHANGELOG.md b/CHANGELOG.md index f6cfa17bcd..c9070f1ecb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ **Features**: - Add inbound filters option to filter legacy Edge browsers (i.e. versions 12-18 ) ([#2650](https://github.com/getsentry/relay/pull/2650)) +- Add User Feedback Ingestion. ([#2604](https://github.com/getsentry/relay/pull/2604)) - Group resource spans by scrubbed domain and filename. ([#2654](https://github.com/getsentry/relay/pull/2654)) - Convert transactions to spans for all organizations. ([#2659](https://github.com/getsentry/relay/pull/2659)) - Filter outliers (>180s) for mobile measurements. ([#2649](https://github.com/getsentry/relay/pull/2649)) diff --git a/py/CHANGELOG.md b/py/CHANGELOG.md index 06ad3aefe7..603953f975 100644 --- a/py/CHANGELOG.md +++ b/py/CHANGELOG.md @@ -7,6 +7,7 @@ - Remove event breadcrumbs dating before January 1, 1970 UTC. ([#2635](https://github.com/getsentry/relay/pull/2635)) - Add `PerformanceScoreConfig` config and performance score calculations to measurements for frontend events. ([#2632](https://github.com/getsentry/relay/pull/2632)) - Add `locale` ,`screen_width_pixels`, `screen_height_pixels`, and `uuid` to the device context. ([#2640](https://github.com/getsentry/relay/pull/2640)) +- Add feedback DataCategory. ([#2604](https://github.com/getsentry/relay/pull/2604)) ## 0.8.32 diff --git a/py/sentry_relay/consts.py b/py/sentry_relay/consts.py index 5f70ada375..4e962a4aea 100644 --- a/py/sentry_relay/consts.py +++ b/py/sentry_relay/consts.py @@ -24,6 +24,7 @@ class DataCategory(IntEnum): PROFILE_INDEXED = 11 SPAN = 12 MONITOR_SEAT = 13 + USER_REPORT_V2 = 14 UNKNOWN = -1 # end generated diff --git a/relay-base-schema/src/data_category.rs b/relay-base-schema/src/data_category.rs index 60adfd3fc2..2268418641 100644 --- a/relay-base-schema/src/data_category.rs +++ b/relay-base-schema/src/data_category.rs @@ -57,6 +57,12 @@ pub enum DataCategory { /// but we define it here to prevent clashing values since this data category enumeration /// is also used outside of Relay via the Python package. MonitorSeat = 13, + /// User Feedback + /// + /// Represents a User Feedback processed. + /// Currently standardized on name UserReportV2 to avoid clashing with the old UserReport. + /// TODO(jferg): Rename this to UserFeedback once old UserReport is deprecated. + UserReportV2 = 14, // // IMPORTANT: After adding a new entry to DataCategory, go to the `relay-cabi` subfolder and run // `make header` to regenerate the C-binding. This allows using the data category from Python. @@ -86,6 +92,7 @@ impl DataCategory { "monitor" => Self::Monitor, "span" => Self::Span, "monitor_seat" => Self::MonitorSeat, + "feedback" => Self::UserReportV2, _ => Self::Unknown, } } @@ -108,6 +115,7 @@ impl DataCategory { Self::Monitor => "monitor", Self::Span => "span", Self::MonitorSeat => "monitor_seat", + Self::UserReportV2 => "feedback", Self::Unknown => "unknown", } } @@ -157,6 +165,7 @@ impl From for DataCategory { EventType::Csp | EventType::Hpkp | EventType::ExpectCt | EventType::ExpectStaple => { Self::Security } + EventType::UserReportV2 => Self::UserReportV2, } } } diff --git a/relay-base-schema/src/events.rs b/relay-base-schema/src/events.rs index 840ed32ef0..0849d6acaa 100644 --- a/relay-base-schema/src/events.rs +++ b/relay-base-schema/src/events.rs @@ -41,6 +41,10 @@ pub enum EventType { ExpectStaple, /// Performance monitoring transactions carrying spans. Transaction, + /// User feedback payload. + /// + /// TODO(Jferg): Change this to UserFeedback once old UserReport logic is deprecated. + UserReportV2, /// All events that do not qualify as any other type. #[serde(other)] #[default] @@ -71,6 +75,7 @@ impl FromStr for EventType { "expectct" => EventType::ExpectCt, "expectstaple" => EventType::ExpectStaple, "transaction" => EventType::Transaction, + "feedback" => EventType::UserReportV2, _ => return Err(ParseEventTypeError), }) } @@ -86,6 +91,7 @@ impl fmt::Display for EventType { EventType::ExpectCt => write!(f, "expectct"), EventType::ExpectStaple => write!(f, "expectstaple"), EventType::Transaction => write!(f, "transaction"), + EventType::UserReportV2 => write!(f, "feedback"), } } } diff --git a/relay-cabi/include/relay.h b/relay-cabi/include/relay.h index 7baed86380..55a1ea451e 100644 --- a/relay-cabi/include/relay.h +++ b/relay-cabi/include/relay.h @@ -3,7 +3,7 @@ #ifndef RELAY_H_INCLUDED #define RELAY_H_INCLUDED -/* Generated with cbindgen:0.25.0 */ +/* Generated with cbindgen:0.26.0 */ /* Warning, this file is autogenerated. Do not modify this manually. */ @@ -88,6 +88,14 @@ enum RelayDataCategory { * is also used outside of Relay via the Python package. */ RELAY_DATA_CATEGORY_MONITOR_SEAT = 13, + /** + * User Feedback + * + * Represents a User Feedback processed. + * Currently standardized on name UserReportV2 to avoid clashing with the old UserReport. + * TODO(jferg): Rename this to UserFeedback once old UserReport is deprecated. + */ + RELAY_DATA_CATEGORY_USER_REPORT_V2 = 14, /** * Any other data category not known by this Relay. */ @@ -616,14 +624,4 @@ struct RelayStr relay_validate_project_config(const struct RelayStr *value, */ struct RelayStr normalize_global_config(const struct RelayStr *value); -/** - * Runs dynamic sampling given the sampling config, root sampling config, DSC and event. - * - * Returns the sampling decision containing the sample_rate and the list of matched rule ids. - */ -struct RelayStr run_dynamic_sampling(const struct RelayStr *sampling_config, - const struct RelayStr *root_sampling_config, - const struct RelayStr *dsc, - const struct RelayStr *event); - #endif /* RELAY_H_INCLUDED */ diff --git a/relay-dynamic-config/src/feature.rs b/relay-dynamic-config/src/feature.rs index 7e8de391b2..ba7f44041e 100644 --- a/relay-dynamic-config/src/feature.rs +++ b/relay-dynamic-config/src/feature.rs @@ -11,6 +11,11 @@ pub enum Feature { /// Enables data scrubbing of replay recording payloads. #[serde(rename = "organizations:session-replay-recording-scrubbing")] SessionReplayRecordingScrubbing, + /// Enables new User Feedback ingest. + /// + /// TODO(jferg): rename to UserFeedbackIngest once old UserReport logic is deprecated. + #[serde(rename = "organizations:user-feedback-ingest")] + UserReportV2Ingest, /// Enables device.class synthesis /// /// Enables device.class tag synthesis on mobile events. diff --git a/relay-event-normalization/src/normalize/mod.rs b/relay-event-normalization/src/normalize/mod.rs index a489fe25e2..4f4728dcc0 100644 --- a/relay-event-normalization/src/normalize/mod.rs +++ b/relay-event-normalization/src/normalize/mod.rs @@ -248,6 +248,9 @@ impl<'a> NormalizeProcessor<'a> { if event.ty.value() == Some(&EventType::Transaction) { return EventType::Transaction; } + if event.ty.value() == Some(&EventType::UserReportV2) { + return EventType::UserReportV2; + } // The SDKs do not describe event types, and we must infer them from available attributes. let has_exceptions = event diff --git a/relay-event-schema/src/protocol/contexts/mod.rs b/relay-event-schema/src/protocol/contexts/mod.rs index d7866c6816..919050efd2 100644 --- a/relay-event-schema/src/protocol/contexts/mod.rs +++ b/relay-event-schema/src/protocol/contexts/mod.rs @@ -12,7 +12,7 @@ mod reprocessing; mod response; mod runtime; mod trace; - +mod user_report_v2; pub use app::*; pub use browser::*; pub use cloud_resource::*; @@ -27,6 +27,7 @@ pub use reprocessing::*; pub use response::*; pub use runtime::*; pub use trace::*; +pub use user_report_v2::*; #[cfg(feature = "jsonschema")] use relay_jsonschema_derive::JsonSchema; @@ -67,6 +68,9 @@ pub enum Context { Profile(Box), /// Information related to Replay. Replay(Box), + /// Information related to User Report V2. TODO:(jferg): rename to UserFeedbackContext + #[metastructure(tag = "feedback")] + UserReportV2(Box), /// Information related to Monitors feature. Monitor(Box), /// Auxilliary information for reprocessing. diff --git a/relay-event-schema/src/protocol/contexts/replay.rs b/relay-event-schema/src/protocol/contexts/replay.rs index 19e8a80310..9ce30b4d12 100644 --- a/relay-event-schema/src/protocol/contexts/replay.rs +++ b/relay-event-schema/src/protocol/contexts/replay.rs @@ -59,7 +59,7 @@ mod tests { use crate::protocol::Context; #[test] - fn test_trace_context_roundtrip() { + fn test_replay_context() { let json = r#"{ "replay_id": "4c79f60c11214eb38604f4ae0781bfb2", "type": "replay" diff --git a/relay-event-schema/src/protocol/contexts/user_report_v2.rs b/relay-event-schema/src/protocol/contexts/user_report_v2.rs new file mode 100644 index 0000000000..323bdcb7f6 --- /dev/null +++ b/relay-event-schema/src/protocol/contexts/user_report_v2.rs @@ -0,0 +1,78 @@ +#[cfg(feature = "jsonschema")] +use relay_jsonschema_derive::JsonSchema; +use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, Value}; + +use crate::processor::ProcessValue; + +/// Feedback context. +/// +/// This contexts contains user feedback specific attributes. +/// We don't PII scrub contact_email as that is provided by the user. +/// TODO(jferg): rename to FeedbackContext once old UserReport logic is deprecated. +#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] +pub struct UserReportV2Context { + /// The feedback message which contains what the user has to say. + pub message: Annotated, + + /// an email optionally provided by the user, which can be different from user.email + #[metastructure(pii = "false")] + pub contact_email: Annotated, + /// Additional arbitrary fields for forwards compatibility. + #[metastructure(additional_properties, retain = "true")] + pub other: Object, +} + +impl super::DefaultContext for UserReportV2Context { + fn default_key() -> &'static str { + "feedback" + } + + fn from_context(context: super::Context) -> Option { + match context { + super::Context::UserReportV2(c) => Some(*c), + _ => None, + } + } + + fn cast(context: &super::Context) -> Option<&Self> { + match context { + super::Context::UserReportV2(c) => Some(c), + _ => None, + } + } + + fn cast_mut(context: &mut super::Context) -> Option<&mut Self> { + match context { + super::Context::UserReportV2(c) => Some(c), + _ => None, + } + } + + fn into_context(self) -> super::Context { + super::Context::UserReportV2(Box::new(self)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::protocol::Context; + + #[test] + fn test_feedback_context() { + let json = r#"{ + "message": "test message", + "contact_email": "test@test.com", + "type": "feedback" +}"#; + let context = Annotated::new(Context::UserReportV2(Box::new(UserReportV2Context { + message: Annotated::new("test message".to_string()), + contact_email: Annotated::new("test@test.com".to_string()), + other: Object::default(), + }))); + + assert_eq!(context, Annotated::from_json(json).unwrap()); + assert_eq!(json, context.to_json_pretty().unwrap()); + } +} diff --git a/relay-quotas/src/quota.rs b/relay-quotas/src/quota.rs index f4f9e9b0af..821b3ef79e 100644 --- a/relay-quotas/src/quota.rs +++ b/relay-quotas/src/quota.rs @@ -110,7 +110,8 @@ impl CategoryUnit { | DataCategory::TransactionIndexed | DataCategory::Span | DataCategory::MonitorSeat - | DataCategory::Monitor => Some(Self::Count), + | DataCategory::Monitor + | DataCategory::UserReportV2 => Some(Self::Count), DataCategory::Attachment => Some(Self::Bytes), DataCategory::Session => Some(Self::Batched), diff --git a/relay-server/src/actors/processor.rs b/relay-server/src/actors/processor.rs index bfb9d98c58..61c072de6b 100644 --- a/relay-server/src/actors/processor.rs +++ b/relay-server/src/actors/processor.rs @@ -1641,6 +1641,7 @@ impl EnvelopeProcessorService { ItemType::Security => true, ItemType::FormData => true, ItemType::RawSecurity => true, + ItemType::UserReportV2 => true, // These should be removed conditionally: ItemType::UnrealReport => self.inner.config.processing_enabled(), @@ -1684,6 +1685,9 @@ impl EnvelopeProcessorService { let transaction_item = envelope.take_item_by(|item| item.ty() == &ItemType::Transaction); let security_item = envelope.take_item_by(|item| item.ty() == &ItemType::Security); let raw_security_item = envelope.take_item_by(|item| item.ty() == &ItemType::RawSecurity); + let user_report_v2_item = + envelope.take_item_by(|item| item.ty() == &ItemType::UserReportV2); + let form_item = envelope.take_item_by(|item| item.ty() == &ItemType::FormData); let attachment_item = envelope .take_item_by(|item| item.attachment_type() == Some(&AttachmentType::EventPayload)); @@ -1715,6 +1719,14 @@ impl EnvelopeProcessorService { // hint to normalization that we're dealing with a transaction now. self.event_from_json_payload(item, Some(EventType::Transaction))? }) + } else if let Some(item) = user_report_v2_item { + relay_log::trace!("processing user_report_v2"); + let project_state = &state.project_state; + let user_report_v2_ingest = project_state.has_feature(Feature::UserReportV2Ingest); + if !user_report_v2_ingest { + return Err(ProcessingError::NoEventPayload); + } + self.event_from_json_payload(item, Some(EventType::UserReportV2))? } else if let Some(mut item) = raw_security_item { relay_log::trace!("processing security report"); sample_rates = item.take_sample_rates(); diff --git a/relay-server/src/actors/store.rs b/relay-server/src/actors/store.rs index 1ac430c5a2..829195478f 100644 --- a/relay-server/src/actors/store.rs +++ b/relay-server/src/actors/store.rs @@ -121,7 +121,10 @@ impl StoreService { let event_item = envelope.get_item_by(|item| { matches!( item.ty(), - ItemType::Event | ItemType::Transaction | ItemType::Security + ItemType::Event + | ItemType::Transaction + | ItemType::Security + | ItemType::UserReportV2 ) }); diff --git a/relay-server/src/envelope.rs b/relay-server/src/envelope.rs index 793c677088..8132ca066e 100644 --- a/relay-server/src/envelope.rs +++ b/relay-server/src/envelope.rs @@ -112,6 +112,8 @@ pub enum ItemType { CheckIn, /// A standalone span. Span, + /// UserReport as an Event + UserReportV2, /// A new item type that is yet unknown by this version of Relay. /// /// By default, items of this type are forwarded without modification. Processing Relays and @@ -127,6 +129,7 @@ impl ItemType { match event_type { EventType::Default | EventType::Error => ItemType::Event, EventType::Transaction => ItemType::Transaction, + EventType::UserReportV2 => ItemType::UserReportV2, EventType::Csp | EventType::Hpkp | EventType::ExpectCt | EventType::ExpectStaple => { ItemType::Security } @@ -145,6 +148,7 @@ impl fmt::Display for ItemType { Self::RawSecurity => write!(f, "raw_security"), Self::UnrealReport => write!(f, "unreal_report"), Self::UserReport => write!(f, "user_report"), + Self::UserReportV2 => write!(f, "feedback"), Self::Session => write!(f, "session"), Self::Sessions => write!(f, "sessions"), Self::Statsd => write!(f, "statsd"), @@ -173,6 +177,7 @@ impl std::str::FromStr for ItemType { "raw_security" => Self::RawSecurity, "unreal_report" => Self::UnrealReport, "user_report" => Self::UserReport, + "feedback" => Self::UserReportV2, "session" => Self::Session, "sessions" => Self::Sessions, "statsd" => Self::Statsd, @@ -556,6 +561,7 @@ impl Item { ItemType::Statsd | ItemType::MetricBuckets => None, ItemType::FormData => None, ItemType::UserReport => None, + ItemType::UserReportV2 => None, ItemType::Profile => Some(if indexed { DataCategory::ProfileIndexed } else { @@ -700,7 +706,8 @@ impl Item { | ItemType::Transaction | ItemType::Security | ItemType::RawSecurity - | ItemType::UnrealReport => true, + | ItemType::UnrealReport + | ItemType::UserReportV2 => true, // Attachments are only event items if they are crash reports or if they carry partial // event payloads. Plain attachments never create event payloads. @@ -758,6 +765,8 @@ impl Item { ItemType::RawSecurity => true, ItemType::UnrealReport => true, ItemType::UserReport => true, + ItemType::UserReportV2 => true, + ItemType::ReplayEvent => true, ItemType::Session => false, ItemType::Sessions => false, diff --git a/relay-server/src/utils/rate_limits.rs b/relay-server/src/utils/rate_limits.rs index 2f6abf6de2..a1aa37c726 100644 --- a/relay-server/src/utils/rate_limits.rs +++ b/relay-server/src/utils/rate_limits.rs @@ -94,6 +94,7 @@ fn infer_event_category(item: &Item) -> Option { ItemType::Transaction => Some(DataCategory::Transaction), ItemType::Security | ItemType::RawSecurity => Some(DataCategory::Security), ItemType::UnrealReport => Some(DataCategory::Error), + ItemType::UserReportV2 => Some(DataCategory::UserReportV2), ItemType::Attachment if item.creates_event() => Some(DataCategory::Error), ItemType::Attachment => None, ItemType::Session => None, diff --git a/relay-server/src/utils/sizes.rs b/relay-server/src/utils/sizes.rs index c73fc41f70..11dd5157c6 100644 --- a/relay-server/src/utils/sizes.rs +++ b/relay-server/src/utils/sizes.rs @@ -29,6 +29,7 @@ pub fn check_envelope_size_limits(config: &Config, envelope: &Envelope) -> bool | ItemType::Security | ItemType::ReplayEvent | ItemType::RawSecurity + | ItemType::UserReportV2 | ItemType::FormData => { event_size += item.len(); } diff --git a/relay-server/tests/snapshots/test_fixtures__event_schema.snap b/relay-server/tests/snapshots/test_fixtures__event_schema.snap index c3e709ecdf..c8af3400e3 100644 --- a/relay-server/tests/snapshots/test_fixtures__event_schema.snap +++ b/relay-server/tests/snapshots/test_fixtures__event_schema.snap @@ -913,6 +913,9 @@ expression: "relay_event_schema::protocol::event_json_schema()" { "$ref": "#/definitions/ReplayContext" }, + { + "$ref": "#/definitions/UserReportV2Context" + }, { "$ref": "#/definitions/MonitorContext" }, @@ -1492,6 +1495,7 @@ expression: "relay_event_schema::protocol::event_json_schema()" "expectct", "expectstaple", "transaction", + "userreportv2", "default" ] }, @@ -3773,6 +3777,33 @@ expression: "relay_event_schema::protocol::event_json_schema()" "additionalProperties": false } ] + }, + "UserReportV2Context": { + "description": " Feedback context.\n\n This contexts contains user feedback specific attributes.\n We don't PII scrub contact_email as that is provided by the user.\n TODO(jferg): rename to FeedbackContext once old UserReport logic is deprecated.", + "anyOf": [ + { + "type": "object", + "properties": { + "contact_email": { + "description": " an email optionally provided by the user, which can be different from user.email", + "default": null, + "type": [ + "string", + "null" + ] + }, + "message": { + "description": " The feedback message which contains what the user has to say.", + "default": null, + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + ] } } } diff --git a/tests/integration/fixtures/__init__.py b/tests/integration/fixtures/__init__.py index 37a76a80ea..99e126bd7f 100644 --- a/tests/integration/fixtures/__init__.py +++ b/tests/integration/fixtures/__init__.py @@ -225,6 +225,11 @@ def send_client_report(self, project_id, payload): envelope.add_item(Item(PayloadRef(json=payload), type="client_report")) self.send_envelope(project_id, envelope) + def send_user_feedback(self, project_id, payload): + envelope = Envelope() + envelope.add_item(Item(PayloadRef(json=payload), type="feedback")) + self.send_envelope(project_id, envelope) + def send_metrics(self, project_id, payload): envelope = Envelope() envelope.add_item( diff --git a/tests/integration/test_feedback.py b/tests/integration/test_feedback.py new file mode 100644 index 0000000000..124baa22d4 --- /dev/null +++ b/tests/integration/test_feedback.py @@ -0,0 +1,122 @@ +import json + + +def generate_feedback_sdk_event(): + return { + "type": "feedback", + "event_id": "d2132d31b39445f1938d7e21b6bf0ec4", + "timestamp": 1597977777.6189718, + "dist": "1.12", + "platform": "javascript", + "environment": "production", + "release": 42, + "tags": {"transaction": "/organizations/:orgId/performance/:eventSlug/"}, + "sdk": {"name": "name", "version": "veresion"}, + "user": { + "id": "123", + "username": "user", + "email": "user@site.com", + "ip_address": "192.168.11.12", + }, + "request": { + "url": None, + "headers": { + "user-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15" + }, + }, + "contexts": { + "feedback": { + "message": "test message", + "contact_email": "test@example.com", + "type": "feedback", + }, + "trace": { + "trace_id": "4C79F60C11214EB38604F4AE0781BFB2", + "span_id": "FA90FDEAD5F74052", + "type": "trace", + }, + "replay": { + "replay_id": "e2d42047b1c5431c8cba85ee2a8ab25d", + }, + }, + } + + +def test_feedback_event_with_processing( + mini_sentry, relay_with_processing, events_consumer +): + relay = relay_with_processing() + mini_sentry.add_basic_project_config( + 42, extra={"config": {"features": ["organizations:user-feedback-ingest"]}} + ) + + _events_consumer = events_consumer(timeout=20) + feedback = generate_feedback_sdk_event() + + relay.send_user_feedback(42, feedback) + + replay_event, replay_event_message = _events_consumer.get_event() + assert replay_event["type"] == "feedback" + # assert replay_event_message["retention_days"] == 90 + + parsed_feedback = json.loads(bytes(replay_event_message["payload"])) + # Assert required fields were returned. + assert parsed_feedback["event_id"] + assert parsed_feedback["type"] == feedback["type"] + assert parsed_feedback["dist"] == feedback["dist"] + assert parsed_feedback["platform"] == feedback["platform"] + assert parsed_feedback["environment"] == feedback["environment"] + assert parsed_feedback["release"] == str(feedback["release"]) + assert parsed_feedback["sdk"]["name"] == feedback["sdk"]["name"] + assert parsed_feedback["sdk"]["version"] == feedback["sdk"]["version"] + assert parsed_feedback["user"]["id"] == feedback["user"]["id"] + assert parsed_feedback["user"]["username"] == feedback["user"]["username"] + assert parsed_feedback["user"]["ip_address"] == feedback["user"]["ip_address"] + + assert parsed_feedback["user"]["email"] == "[email]" + assert parsed_feedback["timestamp"] + + # Assert the tags and requests objects were normalized to lists of doubles. + assert parsed_feedback["tags"] == [["transaction", feedback["tags"]["transaction"]]] + assert parsed_feedback["request"] == { + "headers": [["User-Agent", feedback["request"]["headers"]["user-Agent"]]] + } + + # Assert contexts object was pulled out. + assert parsed_feedback["contexts"] == { + "browser": {"name": "Safari", "version": "15.5", "type": "browser"}, + "device": {"brand": "Apple", "family": "Mac", "model": "Mac", "type": "device"}, + "os": {"name": "Mac OS X", "version": ">=10.15.7", "type": "os"}, + "replay": {"replay_id": "e2d42047b1c5431c8cba85ee2a8ab25d", "type": "replay"}, + "trace": { + "status": "unknown", + "trace_id": "4c79f60c11214eb38604f4ae0781bfb2", + "span_id": "fa90fdead5f74052", + "type": "trace", + }, + "feedback": { + "message": "test message", + "contact_email": "test@example.com", + "type": "feedback", + }, + } + + +def test_feedback_events_without_processing(mini_sentry, relay_chain): + relay = relay_chain(min_relay_version="latest") + + project_id = 42 + mini_sentry.add_basic_project_config( + project_id, + extra={"config": {"features": ["organizations:user-feedback-ingest"]}}, + ) + + replay_item = generate_feedback_sdk_event() + + relay.send_user_feedback(42, replay_item) + + envelope = mini_sentry.captured_events.get(timeout=20) + assert len(envelope.items) == 1 + + userfeedback = envelope.items[0] + assert userfeedback.type == "feedback"