From b910e80471550637f3035bf86d784490b7016aed Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Thu, 21 Apr 2022 17:56:39 -0700 Subject: [PATCH 01/24] feat(replays): add ItemType for recordings --- CHANGELOG.md | 3 +- relay-config/src/config.rs | 6 +++ relay-server/src/actors/envelopes.rs | 1 + relay-server/src/actors/store.rs | 48 ++++++++++++++++- relay-server/src/envelope.rs | 5 ++ relay-server/src/utils/multipart.rs | 9 +++- relay-server/src/utils/rate_limits.rs | 1 + relay-server/src/utils/sizes.rs | 2 +- tests/integration/conftest.py | 1 + tests/integration/fixtures/processing.py | 26 +++++++++ tests/integration/test_replay_recordings.py | 60 +++++++++++++++++++++ 11 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 tests/integration/test_replay_recordings.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 232f04606ea..9d028075d66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ **Internal**: -* Add sampling + tagging by event platform and transaction op. Some (unused) tagging rules from 22.4.0 have been renamed. ([#1231](https://github.com/getsentry/relay/pull/1231)) +- Add sampling + tagging by event platform and transaction op. Some (unused) tagging rules from 22.4.0 have been renamed. ([#1231](https://github.com/getsentry/relay/pull/1231)) +- Add ReplayRecording ItemType. ([#1236](https://github.com/getsentry/relay/pull/1236)) ## 22.4.0 diff --git a/relay-config/src/config.rs b/relay-config/src/config.rs index 15e6e488de7..2af063109da 100644 --- a/relay-config/src/config.rs +++ b/relay-config/src/config.rs @@ -767,6 +767,8 @@ pub enum KafkaTopic { Metrics, /// Profiles Profiles, + /// ReplayRecordings, large blobs sent by the replay sdk + ReplayRecordings, } /// Configuration for topics. @@ -789,6 +791,8 @@ pub struct TopicAssignments { pub metrics: TopicAssignment, /// Stacktrace topic name pub profiles: TopicAssignment, + /// Recordings topic name. + pub replay_recordings: TopicAssignment, } impl TopicAssignments { @@ -803,6 +807,7 @@ impl TopicAssignments { KafkaTopic::Sessions => &self.sessions, KafkaTopic::Metrics => &self.metrics, KafkaTopic::Profiles => &self.profiles, + KafkaTopic::ReplayRecordings => &self.replay_recordings, } } } @@ -818,6 +823,7 @@ impl Default for TopicAssignments { sessions: "ingest-sessions".to_owned().into(), metrics: "ingest-metrics".to_owned().into(), profiles: "profiles".to_owned().into(), + replay_recordings: "ingest-replay-recordings".to_owned().into(), } } } diff --git a/relay-server/src/actors/envelopes.rs b/relay-server/src/actors/envelopes.rs index 846b6b7fcef..5c211f4078a 100644 --- a/relay-server/src/actors/envelopes.rs +++ b/relay-server/src/actors/envelopes.rs @@ -1282,6 +1282,7 @@ impl EnvelopeProcessor { ItemType::MetricBuckets => false, ItemType::ClientReport => false, ItemType::Profile => false, + ItemType::ReplayRecording => false, } } diff --git a/relay-server/src/actors/store.rs b/relay-server/src/actors/store.rs index ba082893694..4b3d9b0b8f2 100644 --- a/relay-server/src/actors/store.rs +++ b/relay-server/src/actors/store.rs @@ -59,6 +59,7 @@ struct Producers { sessions: Producer, metrics: Producer, profiles: Producer, + replay_recordings: Producer, } impl Producers { @@ -76,6 +77,7 @@ impl Producers { KafkaTopic::Sessions => Some(&self.sessions), KafkaTopic::Metrics => Some(&self.metrics), KafkaTopic::Profiles => Some(&self.profiles), + KafkaTopic::ReplayRecordings => Some(&self.replay_recordings), } } } @@ -133,6 +135,11 @@ impl StoreForwarder { sessions: make_producer(&*config, &mut reused_producers, KafkaTopic::Sessions)?, metrics: make_producer(&*config, &mut reused_producers, KafkaTopic::Metrics)?, profiles: make_producer(&*config, &mut reused_producers, KafkaTopic::Profiles)?, + replay_recordings: make_producer( + &*config, + &mut reused_producers, + KafkaTopic::ReplayRecordings, + )?, }; Ok(Self { config, producers }) @@ -164,6 +171,7 @@ impl StoreForwarder { event_id: EventId, project_id: ProjectId, item: &Item, + topic: KafkaTopic, ) -> Result { let id = Uuid::new_v4().to_string(); @@ -185,8 +193,7 @@ impl StoreForwarder { id: id.clone(), chunk_index, }); - - self.produce(KafkaTopic::Attachments, attachment_message)?; + self.produce(topic, attachment_message)?; offset += chunk_size; chunk_index += 1; } @@ -623,6 +630,7 @@ enum KafkaMessage { Session(SessionKafkaMessage), Metric(MetricKafkaMessage), Profile(ProfileKafkaMessage), + ReplayRecording(AttachmentKafkaMessage), } impl KafkaMessage { @@ -635,6 +643,7 @@ impl KafkaMessage { KafkaMessage::Session(_) => "session", KafkaMessage::Metric(_) => "metric", KafkaMessage::Profile(_) => "profile", + KafkaMessage::ReplayRecording(_) => "replay_recording", } } @@ -648,6 +657,7 @@ impl KafkaMessage { Self::Session(_message) => Uuid::nil(), // Explicit random partitioning for sessions Self::Metric(_message) => Uuid::nil(), // TODO(ja): Determine a partitioning key Self::Profile(_message) => Uuid::nil(), + Self::ReplayRecording(_message) => Uuid::nil(), }; if uuid.is_nil() { @@ -690,6 +700,10 @@ fn is_slow_item(item: &Item) -> bool { item.ty() == ItemType::Attachment || item.ty() == ItemType::UserReport } +fn is_replay_recording(item: &Item) -> bool { + item.ty() == ItemType::ReplayRecording +} + impl Handler for StoreForwarder { type Result = Result<(), StoreError>; @@ -714,11 +728,14 @@ impl Handler for StoreForwarder { KafkaTopic::Attachments } else if event_item.map(|x| x.ty()) == Some(ItemType::Transaction) { KafkaTopic::Transactions + } else if envelope.get_item_by(is_replay_recording).is_some() { + KafkaTopic::ReplayRecordings } else { KafkaTopic::Events }; let mut attachments = Vec::new(); + let mut replay_recordings = Vec::new(); for item in envelope.items() { match item.ty() { @@ -728,6 +745,7 @@ impl Handler for StoreForwarder { event_id.ok_or(StoreError::NoEventId)?, scoping.project_id, item, + topic, )?; attachments.push(attachment); } @@ -762,6 +780,16 @@ impl Handler for StoreForwarder { start_time, item, )?, + ItemType::ReplayRecording => { + debug_assert!(topic == KafkaTopic::ReplayRecordings); + let replay_recording = self.produce_attachment_chunks( + event_id.ok_or(StoreError::NoEventId)?, + scoping.project_id, + item, + topic, + )?; + replay_recordings.push(replay_recording); + } _ => {} } } @@ -797,6 +825,22 @@ impl Handler for StoreForwarder { event_type = "attachment" ); } + } else if !replay_recordings.is_empty() { + relay_log::trace!("Sending individual replay_recordings of envelope to kafka"); + for attachment in replay_recordings { + let replay_recording_message = + KafkaMessage::ReplayRecording(AttachmentKafkaMessage { + event_id: event_id.ok_or(StoreError::NoEventId)?, + project_id: scoping.project_id, + attachment, + }); + + self.produce(KafkaTopic::ReplayRecordings, replay_recording_message)?; + metric!( + counter(RelayCounters::ProcessingMessageProduced) += 1, + event_type = "replay_recording" + ); + } } Ok(()) diff --git a/relay-server/src/envelope.rs b/relay-server/src/envelope.rs index c79c3d0b009..157946cc130 100644 --- a/relay-server/src/envelope.rs +++ b/relay-server/src/envelope.rs @@ -104,6 +104,8 @@ pub enum ItemType { ClientReport, /// Profile event payload encoded in JSON Profile, + /// Replay Recording blob payload + ReplayRecording, } impl ItemType { @@ -136,6 +138,7 @@ impl fmt::Display for ItemType { Self::MetricBuckets => write!(f, "metric buckets"), Self::ClientReport => write!(f, "client report"), Self::Profile => write!(f, "profile"), + Self::ReplayRecording => write!(f, "replay_recording"), } } } @@ -580,6 +583,7 @@ impl Item { | ItemType::Metrics | ItemType::MetricBuckets | ItemType::ClientReport + | ItemType::ReplayRecording | ItemType::Profile => false, } } @@ -603,6 +607,7 @@ impl Item { ItemType::MetricBuckets => false, ItemType::ClientReport => false, ItemType::Profile => false, + ItemType::ReplayRecording => false, } } } diff --git a/relay-server/src/utils/multipart.rs b/relay-server/src/utils/multipart.rs index 38fc8ccb0e4..8c99de3d47f 100644 --- a/relay-server/src/utils/multipart.rs +++ b/relay-server/src/utils/multipart.rs @@ -12,6 +12,8 @@ use crate::envelope::{AttachmentType, ContentType, Item, ItemType, Items}; use crate::extractors::DecodingPayload; use crate::service::ServiceState; +const REPLAY_RECORDING_FILENAME: &str = "sentry_replay_rec"; + #[derive(Debug, Fail)] pub enum MultipartError { #[fail(display = "payload reached its size limit")] @@ -218,7 +220,12 @@ fn consume_item( let file_name = content_disposition.as_ref().and_then(|d| d.get_filename()); if let Some(file_name) = file_name { - let mut item = Item::new(ItemType::Attachment); + let item_type = match file_name { + REPLAY_RECORDING_FILENAME => ItemType::ReplayRecording, + _ => ItemType::Attachment, + }; + let mut item = Item::new(item_type); + item.set_attachment_type((*content.infer_type)(field_name)); item.set_payload(content_type.into(), data); item.set_filename(file_name); diff --git a/relay-server/src/utils/rate_limits.rs b/relay-server/src/utils/rate_limits.rs index e35025060ac..12b7f4e60bb 100644 --- a/relay-server/src/utils/rate_limits.rs +++ b/relay-server/src/utils/rate_limits.rs @@ -104,6 +104,7 @@ fn infer_event_category(item: &Item) -> Option { ItemType::FormData => None, ItemType::UserReport => None, ItemType::Profile => None, + ItemType::ReplayRecording => None, // the following items are "internal" item types. From the perspective of the SDK // the use the "internal" data category however this data category is in fact never // supposed to be emitted by relay as internal items must not be rate limited. As diff --git a/relay-server/src/utils/sizes.rs b/relay-server/src/utils/sizes.rs index 8a443fcb0bd..9f129d9c90d 100644 --- a/relay-server/src/utils/sizes.rs +++ b/relay-server/src/utils/sizes.rs @@ -27,7 +27,7 @@ pub fn check_envelope_size_limits(config: &Config, envelope: &Envelope) -> bool | ItemType::Security | ItemType::RawSecurity | ItemType::FormData => event_size += item.len(), - ItemType::Attachment | ItemType::UnrealReport => { + ItemType::Attachment | ItemType::UnrealReport | ItemType::ReplayRecording => { if item.len() > config.max_attachment_size() { return false; } diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 065de26b845..a4d9abb1c6e 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -22,6 +22,7 @@ outcomes_consumer, transactions_consumer, attachments_consumer, + replay_recordings_consumer, sessions_consumer, metrics_consumer, ) # noqa diff --git a/tests/integration/fixtures/processing.py b/tests/integration/fixtures/processing.py index 03138b8336c..9eea86cbdab 100644 --- a/tests/integration/fixtures/processing.py +++ b/tests/integration/fixtures/processing.py @@ -51,6 +51,7 @@ def inner(options=None): "outcomes": get_topic_name("outcomes"), "sessions": get_topic_name("sessions"), "metrics": get_topic_name("metrics"), + "replay_recordings": get_topic_name("replay_recordings"), } if not processing.get("redis"): @@ -280,6 +281,11 @@ def metrics_consumer(kafka_consumer): ) +@pytest.fixture +def replay_recordings_consumer(kafka_consumer): + return lambda: ReplayRecordingsConsumer(*kafka_consumer("replay_recordings")) + + class MetricsConsumer(ConsumerBase): def get_metric(self, timeout=None): message = self.poll(timeout=timeout) @@ -334,3 +340,23 @@ def get_individual_attachment(self): v = msgpack.unpackb(message.value(), raw=False, use_list=False) assert v["type"] == "attachment", v["type"] return v + + +class ReplayRecordingsConsumer(EventsConsumer): + def get_replay_chunk(self): + message = self.poll() + assert message is not None + assert message.error() is None + + v = msgpack.unpackb(message.value(), raw=False, use_list=False) + assert v["type"] == "attachment_chunk", v["type"] + return v["payload"], v + + def get_individual_replay(self): + message = self.poll() + assert message is not None + assert message.error() is None + + v = msgpack.unpackb(message.value(), raw=False, use_list=False) + assert v["type"] == "replay_recording", v["type"] + return v diff --git a/tests/integration/test_replay_recordings.py b/tests/integration/test_replay_recordings.py new file mode 100644 index 00000000000..ec40da7c118 --- /dev/null +++ b/tests/integration/test_replay_recordings.py @@ -0,0 +1,60 @@ +import pytest +import time +import uuid + +from requests.exceptions import HTTPError + + +def test_recording( + mini_sentry, relay_with_processing, replay_recordings_consumer, outcomes_consumer +): + project_id = 42 + event_id = "515539018c9b4260a6f999572f1661ee" + + relay = relay_with_processing() + mini_sentry.add_full_project_config(project_id) + replay_recordings_consumer = replay_recordings_consumer() + outcomes_consumer = outcomes_consumer() + + replay_recordings = [ + ("sentry_replay_rec", "sentry_replay_rec", b"test"), + ] + relay.send_attachments(project_id, event_id, replay_recordings) + + replay_recording_contents = {} + replay_recording_ids = [] + replay_recording_num_chunks = {} + + while set(replay_recording_contents.values()) != {b"test"}: + chunk, v = replay_recordings_consumer.get_replay_chunk() + replay_recording_contents[v["id"]] = ( + replay_recording_contents.get(v["id"], b"") + chunk + ) + if v["id"] not in replay_recording_ids: + replay_recording_ids.append(v["id"]) + num_chunks = 1 + replay_recording_num_chunks.get(v["id"], 0) + assert v["chunk_index"] == num_chunks - 1 + replay_recording_num_chunks[v["id"]] = num_chunks + + id1 = replay_recording_ids[0] + + assert replay_recording_contents[id1] == b"test" + + replay_recording = replay_recordings_consumer.get_individual_replay() + + assert replay_recording == { + "type": "replay_recording", + "attachment": { + "attachment_type": "event.attachment", + "chunks": replay_recording_num_chunks[id1], + "content_type": "application/octet-stream", + "id": id1, + "name": "sentry_replay_rec", + "size": len(replay_recording_contents[id1]), + "rate_limited": False, + }, + "event_id": event_id, + "project_id": project_id, + } + + outcomes_consumer.assert_empty() From d81ad3a09adb086f6a8360cf02dd6b1e0d2e2c9a Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Wed, 27 Apr 2022 10:17:39 -0700 Subject: [PATCH 02/24] name change recording -> payload --- CHANGELOG.md | 3 +- relay-config/src/config.rs | 12 ++--- relay-server/src/actors/envelopes.rs | 2 +- relay-server/src/actors/store.rs | 55 ++++++++++--------- relay-server/src/envelope.rs | 10 ++-- relay-server/src/utils/multipart.rs | 4 +- relay-server/src/utils/rate_limits.rs | 2 +- relay-server/src/utils/sizes.rs | 2 +- tests/integration/conftest.py | 2 +- tests/integration/fixtures/processing.py | 10 ++-- tests/integration/test_replay_payloads.py | 60 +++++++++++++++++++++ tests/integration/test_replay_recordings.py | 60 --------------------- 12 files changed, 110 insertions(+), 112 deletions(-) create mode 100644 tests/integration/test_replay_payloads.py delete mode 100644 tests/integration/test_replay_recordings.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ad8e9e617ed..7b24c42dbe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,7 @@ - Refactor aggregation error, recover from errors more gracefully. ([#1240](https://github.com/getsentry/relay/pull/1240)) - Remove/reject nul-bytes from metric strings. ([#1235](https://github.com/getsentry/relay/pull/1235)) - Remove the unused "internal" data category. ([#1245](https://github.com/getsentry/relay/pull/1245)) -- Add ReplayRecording ItemType. ([#1236](https://github.com/getsentry/relay/pull/1236)) - +- Add ReplayPayload ItemType. ([#1236](https://github.com/getsentry/relay/pull/1236)) ## 22.4.0 diff --git a/relay-config/src/config.rs b/relay-config/src/config.rs index 2af063109da..62f181d4007 100644 --- a/relay-config/src/config.rs +++ b/relay-config/src/config.rs @@ -767,8 +767,8 @@ pub enum KafkaTopic { Metrics, /// Profiles Profiles, - /// ReplayRecordings, large blobs sent by the replay sdk - ReplayRecordings, + /// ReplayPayloads, large blobs sent by the replay sdk + ReplayPayloads, } /// Configuration for topics. @@ -791,8 +791,8 @@ pub struct TopicAssignments { pub metrics: TopicAssignment, /// Stacktrace topic name pub profiles: TopicAssignment, - /// Recordings topic name. - pub replay_recordings: TopicAssignment, + /// Payloads topic name. + pub replay_payloads: TopicAssignment, } impl TopicAssignments { @@ -807,7 +807,7 @@ impl TopicAssignments { KafkaTopic::Sessions => &self.sessions, KafkaTopic::Metrics => &self.metrics, KafkaTopic::Profiles => &self.profiles, - KafkaTopic::ReplayRecordings => &self.replay_recordings, + KafkaTopic::ReplayPayloads => &self.replay_payloads, } } } @@ -823,7 +823,7 @@ impl Default for TopicAssignments { sessions: "ingest-sessions".to_owned().into(), metrics: "ingest-metrics".to_owned().into(), profiles: "profiles".to_owned().into(), - replay_recordings: "ingest-replay-recordings".to_owned().into(), + replay_payloads: "ingest-replay-payloads".to_owned().into(), } } } diff --git a/relay-server/src/actors/envelopes.rs b/relay-server/src/actors/envelopes.rs index 5c211f4078a..8ee4c68d747 100644 --- a/relay-server/src/actors/envelopes.rs +++ b/relay-server/src/actors/envelopes.rs @@ -1282,7 +1282,7 @@ impl EnvelopeProcessor { ItemType::MetricBuckets => false, ItemType::ClientReport => false, ItemType::Profile => false, - ItemType::ReplayRecording => false, + ItemType::ReplayPayload => false, } } diff --git a/relay-server/src/actors/store.rs b/relay-server/src/actors/store.rs index 4b3d9b0b8f2..fba02acdbdd 100644 --- a/relay-server/src/actors/store.rs +++ b/relay-server/src/actors/store.rs @@ -59,7 +59,7 @@ struct Producers { sessions: Producer, metrics: Producer, profiles: Producer, - replay_recordings: Producer, + replay_payloads: Producer, } impl Producers { @@ -77,7 +77,7 @@ impl Producers { KafkaTopic::Sessions => Some(&self.sessions), KafkaTopic::Metrics => Some(&self.metrics), KafkaTopic::Profiles => Some(&self.profiles), - KafkaTopic::ReplayRecordings => Some(&self.replay_recordings), + KafkaTopic::ReplayPayloads => Some(&self.replay_payloads), } } } @@ -135,10 +135,10 @@ impl StoreForwarder { sessions: make_producer(&*config, &mut reused_producers, KafkaTopic::Sessions)?, metrics: make_producer(&*config, &mut reused_producers, KafkaTopic::Metrics)?, profiles: make_producer(&*config, &mut reused_producers, KafkaTopic::Profiles)?, - replay_recordings: make_producer( + replay_payloads: make_producer( &*config, &mut reused_producers, - KafkaTopic::ReplayRecordings, + KafkaTopic::ReplayPayloads, )?, }; @@ -630,7 +630,7 @@ enum KafkaMessage { Session(SessionKafkaMessage), Metric(MetricKafkaMessage), Profile(ProfileKafkaMessage), - ReplayRecording(AttachmentKafkaMessage), + ReplayPayload(AttachmentKafkaMessage), } impl KafkaMessage { @@ -643,7 +643,7 @@ impl KafkaMessage { KafkaMessage::Session(_) => "session", KafkaMessage::Metric(_) => "metric", KafkaMessage::Profile(_) => "profile", - KafkaMessage::ReplayRecording(_) => "replay_recording", + KafkaMessage::ReplayPayload(_) => "replay_payload", } } @@ -657,7 +657,7 @@ impl KafkaMessage { Self::Session(_message) => Uuid::nil(), // Explicit random partitioning for sessions Self::Metric(_message) => Uuid::nil(), // TODO(ja): Determine a partitioning key Self::Profile(_message) => Uuid::nil(), - Self::ReplayRecording(_message) => Uuid::nil(), + Self::ReplayPayload(_message) => Uuid::nil(), }; if uuid.is_nil() { @@ -700,8 +700,8 @@ fn is_slow_item(item: &Item) -> bool { item.ty() == ItemType::Attachment || item.ty() == ItemType::UserReport } -fn is_replay_recording(item: &Item) -> bool { - item.ty() == ItemType::ReplayRecording +fn is_replay_payload(item: &Item) -> bool { + item.ty() == ItemType::ReplayPayload } impl Handler for StoreForwarder { @@ -728,14 +728,14 @@ impl Handler for StoreForwarder { KafkaTopic::Attachments } else if event_item.map(|x| x.ty()) == Some(ItemType::Transaction) { KafkaTopic::Transactions - } else if envelope.get_item_by(is_replay_recording).is_some() { - KafkaTopic::ReplayRecordings + } else if envelope.get_item_by(is_replay_payload).is_some() { + KafkaTopic::ReplayPayloads } else { KafkaTopic::Events }; let mut attachments = Vec::new(); - let mut replay_recordings = Vec::new(); + let mut replay_payloads = Vec::new(); for item in envelope.items() { match item.ty() { @@ -780,15 +780,15 @@ impl Handler for StoreForwarder { start_time, item, )?, - ItemType::ReplayRecording => { - debug_assert!(topic == KafkaTopic::ReplayRecordings); - let replay_recording = self.produce_attachment_chunks( + ItemType::ReplayPayload => { + debug_assert!(topic == KafkaTopic::ReplayPayloads); + let replay_payload = self.produce_attachment_chunks( event_id.ok_or(StoreError::NoEventId)?, scoping.project_id, item, topic, )?; - replay_recordings.push(replay_recording); + replay_payloads.push(replay_payload); } _ => {} } @@ -825,20 +825,19 @@ impl Handler for StoreForwarder { event_type = "attachment" ); } - } else if !replay_recordings.is_empty() { - relay_log::trace!("Sending individual replay_recordings of envelope to kafka"); - for attachment in replay_recordings { - let replay_recording_message = - KafkaMessage::ReplayRecording(AttachmentKafkaMessage { - event_id: event_id.ok_or(StoreError::NoEventId)?, - project_id: scoping.project_id, - attachment, - }); - - self.produce(KafkaTopic::ReplayRecordings, replay_recording_message)?; + } else if !replay_payloads.is_empty() { + relay_log::trace!("Sending individual replay_payloads of envelope to kafka"); + for attachment in replay_payloads { + let replay_payload_message = KafkaMessage::ReplayPayload(AttachmentKafkaMessage { + event_id: event_id.ok_or(StoreError::NoEventId)?, + project_id: scoping.project_id, + attachment, + }); + + self.produce(KafkaTopic::ReplayPayloads, replay_payload_message)?; metric!( counter(RelayCounters::ProcessingMessageProduced) += 1, - event_type = "replay_recording" + event_type = "replay_payload" ); } } diff --git a/relay-server/src/envelope.rs b/relay-server/src/envelope.rs index 157946cc130..85caffb24f1 100644 --- a/relay-server/src/envelope.rs +++ b/relay-server/src/envelope.rs @@ -104,8 +104,8 @@ pub enum ItemType { ClientReport, /// Profile event payload encoded in JSON Profile, - /// Replay Recording blob payload - ReplayRecording, + /// Replay Payload blob payload + ReplayPayload, } impl ItemType { @@ -138,7 +138,7 @@ impl fmt::Display for ItemType { Self::MetricBuckets => write!(f, "metric buckets"), Self::ClientReport => write!(f, "client report"), Self::Profile => write!(f, "profile"), - Self::ReplayRecording => write!(f, "replay_recording"), + Self::ReplayPayload => write!(f, "replay_payload"), } } } @@ -583,7 +583,7 @@ impl Item { | ItemType::Metrics | ItemType::MetricBuckets | ItemType::ClientReport - | ItemType::ReplayRecording + | ItemType::ReplayPayload | ItemType::Profile => false, } } @@ -607,7 +607,7 @@ impl Item { ItemType::MetricBuckets => false, ItemType::ClientReport => false, ItemType::Profile => false, - ItemType::ReplayRecording => false, + ItemType::ReplayPayload => false, } } } diff --git a/relay-server/src/utils/multipart.rs b/relay-server/src/utils/multipart.rs index 8c99de3d47f..264c3a3a39a 100644 --- a/relay-server/src/utils/multipart.rs +++ b/relay-server/src/utils/multipart.rs @@ -12,7 +12,7 @@ use crate::envelope::{AttachmentType, ContentType, Item, ItemType, Items}; use crate::extractors::DecodingPayload; use crate::service::ServiceState; -const REPLAY_RECORDING_FILENAME: &str = "sentry_replay_rec"; +const REPLAY_RECORDING_FILENAME: &str = "sentry_replay_payload"; #[derive(Debug, Fail)] pub enum MultipartError { @@ -221,7 +221,7 @@ fn consume_item( if let Some(file_name) = file_name { let item_type = match file_name { - REPLAY_RECORDING_FILENAME => ItemType::ReplayRecording, + REPLAY_RECORDING_FILENAME => ItemType::ReplayPayload, _ => ItemType::Attachment, }; let mut item = Item::new(item_type); diff --git a/relay-server/src/utils/rate_limits.rs b/relay-server/src/utils/rate_limits.rs index d1d9a36c8c2..e2988550f07 100644 --- a/relay-server/src/utils/rate_limits.rs +++ b/relay-server/src/utils/rate_limits.rs @@ -104,7 +104,7 @@ fn infer_event_category(item: &Item) -> Option { ItemType::FormData => None, ItemType::UserReport => None, ItemType::Profile => None, - ItemType::ReplayRecording => None, + ItemType::ReplayPayload => None, ItemType::ClientReport => None, } } diff --git a/relay-server/src/utils/sizes.rs b/relay-server/src/utils/sizes.rs index 9f129d9c90d..88ffbbfc8da 100644 --- a/relay-server/src/utils/sizes.rs +++ b/relay-server/src/utils/sizes.rs @@ -27,7 +27,7 @@ pub fn check_envelope_size_limits(config: &Config, envelope: &Envelope) -> bool | ItemType::Security | ItemType::RawSecurity | ItemType::FormData => event_size += item.len(), - ItemType::Attachment | ItemType::UnrealReport | ItemType::ReplayRecording => { + ItemType::Attachment | ItemType::UnrealReport | ItemType::ReplayPayload => { if item.len() > config.max_attachment_size() { return false; } diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index a4d9abb1c6e..d534b42f20a 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -22,7 +22,7 @@ outcomes_consumer, transactions_consumer, attachments_consumer, - replay_recordings_consumer, + replay_payloads_consumer, sessions_consumer, metrics_consumer, ) # noqa diff --git a/tests/integration/fixtures/processing.py b/tests/integration/fixtures/processing.py index 9eea86cbdab..96493b61ff3 100644 --- a/tests/integration/fixtures/processing.py +++ b/tests/integration/fixtures/processing.py @@ -51,7 +51,7 @@ def inner(options=None): "outcomes": get_topic_name("outcomes"), "sessions": get_topic_name("sessions"), "metrics": get_topic_name("metrics"), - "replay_recordings": get_topic_name("replay_recordings"), + "replay_payloads": get_topic_name("replay_payloads"), } if not processing.get("redis"): @@ -282,8 +282,8 @@ def metrics_consumer(kafka_consumer): @pytest.fixture -def replay_recordings_consumer(kafka_consumer): - return lambda: ReplayRecordingsConsumer(*kafka_consumer("replay_recordings")) +def replay_payloads_consumer(kafka_consumer): + return lambda: ReplayPayloadsConsumer(*kafka_consumer("replay_payloads")) class MetricsConsumer(ConsumerBase): @@ -342,7 +342,7 @@ def get_individual_attachment(self): return v -class ReplayRecordingsConsumer(EventsConsumer): +class ReplayPayloadsConsumer(EventsConsumer): def get_replay_chunk(self): message = self.poll() assert message is not None @@ -358,5 +358,5 @@ def get_individual_replay(self): assert message.error() is None v = msgpack.unpackb(message.value(), raw=False, use_list=False) - assert v["type"] == "replay_recording", v["type"] + assert v["type"] == "replay_payload", v["type"] return v diff --git a/tests/integration/test_replay_payloads.py b/tests/integration/test_replay_payloads.py new file mode 100644 index 00000000000..10ee4a9e879 --- /dev/null +++ b/tests/integration/test_replay_payloads.py @@ -0,0 +1,60 @@ +import pytest +import time +import uuid + +from requests.exceptions import HTTPError + + +def test_payload( + mini_sentry, relay_with_processing, replay_payloads_consumer, outcomes_consumer +): + project_id = 42 + event_id = "515539018c9b4260a6f999572f1661ee" + + relay = relay_with_processing() + mini_sentry.add_full_project_config(project_id) + replay_payloads_consumer = replay_payloads_consumer() + outcomes_consumer = outcomes_consumer() + + replay_payloads = [ + ("sentry_replay_payload", "sentry_replay_payload", b"test"), + ] + relay.send_attachments(project_id, event_id, replay_payloads) + + replay_payload_contents = {} + replay_payload_ids = [] + replay_payload_num_chunks = {} + + while set(replay_payload_contents.values()) != {b"test"}: + chunk, v = replay_payloads_consumer.get_replay_chunk() + replay_payload_contents[v["id"]] = ( + replay_payload_contents.get(v["id"], b"") + chunk + ) + if v["id"] not in replay_payload_ids: + replay_payload_ids.append(v["id"]) + num_chunks = 1 + replay_payload_num_chunks.get(v["id"], 0) + assert v["chunk_index"] == num_chunks - 1 + replay_payload_num_chunks[v["id"]] = num_chunks + + id1 = replay_payload_ids[0] + + assert replay_payload_contents[id1] == b"test" + + replay_payload = replay_payloads_consumer.get_individual_replay() + + assert replay_payload == { + "type": "replay_payload", + "attachment": { + "attachment_type": "event.attachment", + "chunks": replay_payload_num_chunks[id1], + "content_type": "application/octet-stream", + "id": id1, + "name": "sentry_replay_payload", + "size": len(replay_payload_contents[id1]), + "rate_limited": False, + }, + "event_id": event_id, + "project_id": project_id, + } + + outcomes_consumer.assert_empty() diff --git a/tests/integration/test_replay_recordings.py b/tests/integration/test_replay_recordings.py deleted file mode 100644 index ec40da7c118..00000000000 --- a/tests/integration/test_replay_recordings.py +++ /dev/null @@ -1,60 +0,0 @@ -import pytest -import time -import uuid - -from requests.exceptions import HTTPError - - -def test_recording( - mini_sentry, relay_with_processing, replay_recordings_consumer, outcomes_consumer -): - project_id = 42 - event_id = "515539018c9b4260a6f999572f1661ee" - - relay = relay_with_processing() - mini_sentry.add_full_project_config(project_id) - replay_recordings_consumer = replay_recordings_consumer() - outcomes_consumer = outcomes_consumer() - - replay_recordings = [ - ("sentry_replay_rec", "sentry_replay_rec", b"test"), - ] - relay.send_attachments(project_id, event_id, replay_recordings) - - replay_recording_contents = {} - replay_recording_ids = [] - replay_recording_num_chunks = {} - - while set(replay_recording_contents.values()) != {b"test"}: - chunk, v = replay_recordings_consumer.get_replay_chunk() - replay_recording_contents[v["id"]] = ( - replay_recording_contents.get(v["id"], b"") + chunk - ) - if v["id"] not in replay_recording_ids: - replay_recording_ids.append(v["id"]) - num_chunks = 1 + replay_recording_num_chunks.get(v["id"], 0) - assert v["chunk_index"] == num_chunks - 1 - replay_recording_num_chunks[v["id"]] = num_chunks - - id1 = replay_recording_ids[0] - - assert replay_recording_contents[id1] == b"test" - - replay_recording = replay_recordings_consumer.get_individual_replay() - - assert replay_recording == { - "type": "replay_recording", - "attachment": { - "attachment_type": "event.attachment", - "chunks": replay_recording_num_chunks[id1], - "content_type": "application/octet-stream", - "id": id1, - "name": "sentry_replay_rec", - "size": len(replay_recording_contents[id1]), - "rate_limited": False, - }, - "event_id": event_id, - "project_id": project_id, - } - - outcomes_consumer.assert_empty() From e9972c54aa2ed558fb024edecf88c9477d1dd5af Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Fri, 22 Apr 2022 11:36:20 -0700 Subject: [PATCH 03/24] feat(replays): add replay events itemtype --- CHANGELOG.md | 3 +- relay-common/src/constants.rs | 8 ++ relay-config/src/config.rs | 6 ++ relay-general/src/store/normalize.rs | 11 ++- .../test_fixtures__event_schema.snap | 1 + relay-quotas/src/quota.rs | 1 + relay-server/src/actors/envelopes.rs | 8 ++ relay-server/src/actors/store.rs | 49 ++++++++++++ relay-server/src/envelope.rs | 6 ++ .../src/metrics_extraction/transactions.rs | 4 +- relay-server/src/utils/rate_limits.rs | 1 + relay-server/src/utils/sizes.rs | 1 + tests/integration/conftest.py | 3 +- tests/integration/fixtures/__init__.py | 9 ++- tests/integration/fixtures/processing.py | 17 ++++ tests/integration/test_replay_event.py | 79 +++++++++++++++++++ 16 files changed, 201 insertions(+), 6 deletions(-) create mode 100644 tests/integration/test_replay_event.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b24c42dbe1..2e6c466744a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ - Refactor aggregation error, recover from errors more gracefully. ([#1240](https://github.com/getsentry/relay/pull/1240)) - Remove/reject nul-bytes from metric strings. ([#1235](https://github.com/getsentry/relay/pull/1235)) - Remove the unused "internal" data category. ([#1245](https://github.com/getsentry/relay/pull/1245)) -- Add ReplayPayload ItemType. ([#1236](https://github.com/getsentry/relay/pull/1236)) +- Add ReplayRecording ItemType. ([#1236](https://github.com/getsentry/relay/pull/1236)) +- Add ReplayEvent ItemType. ([#1239](https://github.com/getsentry/relay/pull/1239)) ## 22.4.0 diff --git a/relay-common/src/constants.rs b/relay-common/src/constants.rs index de4a0d66fa7..8a00171388d 100644 --- a/relay-common/src/constants.rs +++ b/relay-common/src/constants.rs @@ -36,6 +36,8 @@ pub enum EventType { ExpectStaple, /// Performance monitoring transactions carrying spans. Transaction, + /// Replay Events for session replays + ReplayEvent, /// All events that do not qualify as any other type. #[serde(other)] Default, @@ -86,6 +88,7 @@ impl fmt::Display for EventType { EventType::ExpectCt => write!(f, "expectct"), EventType::ExpectStaple => write!(f, "expectstaple"), EventType::Transaction => write!(f, "transaction"), + EventType::ReplayEvent => write!(f, "replay_event"), } } } @@ -107,6 +110,8 @@ pub enum DataCategory { Attachment = 4, /// Session updates. Quantity is the number of updates in the batch. Session = 5, + /// Replay Events + ReplayEvent = 6, /// Any other data category not known by this Relay. #[serde(other)] Unknown = -1, @@ -122,6 +127,7 @@ impl DataCategory { "security" => Self::Security, "attachment" => Self::Attachment, "session" => Self::Session, + "replay_event" => Self::ReplayEvent, _ => Self::Unknown, } } @@ -135,6 +141,7 @@ impl DataCategory { Self::Security => "security", Self::Attachment => "attachment", Self::Session => "session", + Self::ReplayEvent => "replay_event", Self::Unknown => "unknown", } } @@ -171,6 +178,7 @@ impl From for DataCategory { match ty { EventType::Default | EventType::Error => Self::Error, EventType::Transaction => Self::Transaction, + EventType::ReplayEvent => Self::ReplayEvent, EventType::Csp | EventType::Hpkp | EventType::ExpectCt | EventType::ExpectStaple => { Self::Security } diff --git a/relay-config/src/config.rs b/relay-config/src/config.rs index 62f181d4007..1635b77a36b 100644 --- a/relay-config/src/config.rs +++ b/relay-config/src/config.rs @@ -769,6 +769,8 @@ pub enum KafkaTopic { Profiles, /// ReplayPayloads, large blobs sent by the replay sdk ReplayPayloads, + /// ReplayEvents, breadcrumb + session updates for replays + ReplayEvents, } /// Configuration for topics. @@ -793,6 +795,8 @@ pub struct TopicAssignments { pub profiles: TopicAssignment, /// Payloads topic name. pub replay_payloads: TopicAssignment, + /// Replay Events topic name. + pub replay_events: TopicAssignment, } impl TopicAssignments { @@ -808,6 +812,7 @@ impl TopicAssignments { KafkaTopic::Metrics => &self.metrics, KafkaTopic::Profiles => &self.profiles, KafkaTopic::ReplayPayloads => &self.replay_payloads, + KafkaTopic::ReplayEvents => &self.replay_events, } } } @@ -824,6 +829,7 @@ impl Default for TopicAssignments { metrics: "ingest-metrics".to_owned().into(), profiles: "profiles".to_owned().into(), replay_payloads: "ingest-replay-payloads".to_owned().into(), + replay_events: "ingest-replay-events".to_owned().into(), } } } diff --git a/relay-general/src/store/normalize.rs b/relay-general/src/store/normalize.rs index b269fbf77c0..9bddbc0030d 100644 --- a/relay-general/src/store/normalize.rs +++ b/relay-general/src/store/normalize.rs @@ -114,7 +114,9 @@ impl<'a> NormalizeProcessor<'a> { /// Ensure measurements interface is only present for transaction events fn normalize_measurements(&self, event: &mut Event) { - if event.ty.value() != Some(&EventType::Transaction) { + if event.ty.value() != Some(&EventType::Transaction) + && event.ty.value() != Some(&EventType::ReplayEvent) + { // Only transaction events may have a measurements interface event.measurements = Annotated::empty(); } @@ -129,7 +131,9 @@ impl<'a> NormalizeProcessor<'a> { } fn normalize_spans(&self, event: &mut Event) { - if event.ty.value() == Some(&EventType::Transaction) { + if event.ty.value() == Some(&EventType::Transaction) + || event.ty.value() == Some(&EventType::ReplayEvent) + { spans::normalize_spans(event, &self.config.span_attributes); } } @@ -260,6 +264,9 @@ impl<'a> NormalizeProcessor<'a> { if event.ty.value() == Some(&EventType::Transaction) { return EventType::Transaction; } + if event.ty.value() == Some(&EventType::ReplayEvent) { + return EventType::ReplayEvent; + } // The SDKs do not describe event types, and we must infer them from available attributes. let has_exceptions = event diff --git a/relay-general/tests/snapshots/test_fixtures__event_schema.snap b/relay-general/tests/snapshots/test_fixtures__event_schema.snap index 122693084e4..ed814968ea7 100644 --- a/relay-general/tests/snapshots/test_fixtures__event_schema.snap +++ b/relay-general/tests/snapshots/test_fixtures__event_schema.snap @@ -1228,6 +1228,7 @@ expression: event_json_schema() "expectct", "expectstaple", "transaction", + "replayevent", "default" ] }, diff --git a/relay-quotas/src/quota.rs b/relay-quotas/src/quota.rs index ae21042b445..8811f654ec3 100644 --- a/relay-quotas/src/quota.rs +++ b/relay-quotas/src/quota.rs @@ -103,6 +103,7 @@ impl CategoryUnit { DataCategory::Default | DataCategory::Error | DataCategory::Transaction + | DataCategory::ReplayEvent | DataCategory::Security => Some(Self::Count), DataCategory::Attachment => Some(Self::Bytes), DataCategory::Session => Some(Self::Batched), diff --git a/relay-server/src/actors/envelopes.rs b/relay-server/src/actors/envelopes.rs index 8ee4c68d747..ccee0ea87f6 100644 --- a/relay-server/src/actors/envelopes.rs +++ b/relay-server/src/actors/envelopes.rs @@ -1283,6 +1283,7 @@ impl EnvelopeProcessor { ItemType::ClientReport => false, ItemType::Profile => false, ItemType::ReplayPayload => false, + ItemType::ReplayEvent => false, } } @@ -1302,6 +1303,7 @@ impl EnvelopeProcessor { // `process_event`. let event_item = envelope.take_item_by(|item| item.ty() == ItemType::Event); let transaction_item = envelope.take_item_by(|item| item.ty() == ItemType::Transaction); + let replay_item = envelope.take_item_by(|item| item.ty() == ItemType::ReplayEvent); 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 form_item = envelope.take_item_by(|item| item.ty() == ItemType::FormData); @@ -1333,6 +1335,12 @@ impl EnvelopeProcessor { // hint to normalization that we're dealing with a transaction now. self.event_from_json_payload(item, Some(EventType::Transaction))? }) + } else if let Some(mut item) = replay_item { + relay_log::trace!("processing json replay event"); + state.sample_rates = item.take_sample_rates(); + metric!(timer(RelayTimers::EventProcessingDeserialize), { + self.event_from_json_payload(item, Some(EventType::ReplayEvent))? + }) } else if let Some(mut item) = raw_security_item { relay_log::trace!("processing security report"); state.sample_rates = item.take_sample_rates(); diff --git a/relay-server/src/actors/store.rs b/relay-server/src/actors/store.rs index fba02acdbdd..9f72268e797 100644 --- a/relay-server/src/actors/store.rs +++ b/relay-server/src/actors/store.rs @@ -60,6 +60,7 @@ struct Producers { metrics: Producer, profiles: Producer, replay_payloads: Producer, + replay_events: Producer, } impl Producers { @@ -78,6 +79,7 @@ impl Producers { KafkaTopic::Metrics => Some(&self.metrics), KafkaTopic::Profiles => Some(&self.profiles), KafkaTopic::ReplayPayloads => Some(&self.replay_payloads), + KafkaTopic::ReplayEvents => Some(&self.replay_events), } } } @@ -140,6 +142,11 @@ impl StoreForwarder { &mut reused_producers, KafkaTopic::ReplayPayloads, )?, + replay_events: make_producer( + &*config, + &mut reused_producers, + KafkaTopic::ReplayEvents, + )?, }; Ok(Self { config, producers }) @@ -193,6 +200,7 @@ impl StoreForwarder { id: id.clone(), chunk_index, }); + self.produce(topic, attachment_message)?; offset += chunk_size; chunk_index += 1; @@ -444,6 +452,27 @@ impl StoreForwarder { ); Ok(()) } + fn produce_replay_event( + &self, + event_id: EventId, + project_id: ProjectId, + start_time: Instant, + item: &Item, + ) -> Result<(), StoreError> { + let message = ReplayEventKafkaMessage { + event_id, + project_id, + start_time: UnixTimestamp::from_instant(start_time).as_secs(), + payload: item.payload(), + }; + relay_log::trace!("Sending replay event to Kafka"); + self.produce(KafkaTopic::ReplayEvents, KafkaMessage::ReplayEvent(message))?; + metric!( + counter(RelayCounters::ProcessingMessageProduced) += 1, + event_type = "replay_event" + ); + Ok(()) + } } /// StoreMessageForwarder is an async actor since the only thing it does is put the messages @@ -531,6 +560,17 @@ struct EventKafkaMessage { /// Attachments that are potentially relevant for processing. attachments: Vec, } +#[derive(Clone, Debug, Serialize)] +struct ReplayEventKafkaMessage { + /// Raw event payload. + payload: Bytes, + /// Time at which the event was received by Relay. + start_time: u64, + /// The event id. + event_id: EventId, + /// The project id for the current event. + project_id: ProjectId, +} /// Container payload for chunks of attachments. #[derive(Debug, Serialize)] @@ -631,6 +671,7 @@ enum KafkaMessage { Metric(MetricKafkaMessage), Profile(ProfileKafkaMessage), ReplayPayload(AttachmentKafkaMessage), + ReplayEvent(ReplayEventKafkaMessage), } impl KafkaMessage { @@ -644,6 +685,7 @@ impl KafkaMessage { KafkaMessage::Metric(_) => "metric", KafkaMessage::Profile(_) => "profile", KafkaMessage::ReplayPayload(_) => "replay_payload", + KafkaMessage::ReplayEvent(_) => "replay_event", } } @@ -658,6 +700,7 @@ impl KafkaMessage { Self::Metric(_message) => Uuid::nil(), // TODO(ja): Determine a partitioning key Self::Profile(_message) => Uuid::nil(), Self::ReplayPayload(_message) => Uuid::nil(), + Self::ReplayEvent(_message) => Uuid::nil(), }; if uuid.is_nil() { @@ -790,6 +833,12 @@ impl Handler for StoreForwarder { )?; replay_payloads.push(replay_payload); } + ItemType::ReplayEvent => self.produce_replay_event( + event_id.ok_or(StoreError::NoEventId)?, + scoping.project_id, + start_time, + item, + )?, _ => {} } } diff --git a/relay-server/src/envelope.rs b/relay-server/src/envelope.rs index 85caffb24f1..7c15332ebea 100644 --- a/relay-server/src/envelope.rs +++ b/relay-server/src/envelope.rs @@ -106,6 +106,8 @@ pub enum ItemType { Profile, /// Replay Payload blob payload ReplayPayload, + /// Replay metadata and breadcrumb payload + ReplayEvent, } impl ItemType { @@ -114,6 +116,7 @@ impl ItemType { match event_type { EventType::Default | EventType::Error => ItemType::Event, EventType::Transaction => ItemType::Transaction, + EventType::ReplayEvent => ItemType::ReplayEvent, EventType::Csp | EventType::Hpkp | EventType::ExpectCt | EventType::ExpectStaple => { ItemType::Security } @@ -139,6 +142,7 @@ impl fmt::Display for ItemType { Self::ClientReport => write!(f, "client report"), Self::Profile => write!(f, "profile"), Self::ReplayPayload => write!(f, "replay_payload"), + Self::ReplayEvent => write!(f, "replay_event"), } } } @@ -557,6 +561,7 @@ impl Item { | ItemType::Transaction | ItemType::Security | ItemType::RawSecurity + | ItemType::ReplayEvent | ItemType::UnrealReport => true, // Attachments are only event items if they are crash reports or if they carry partial @@ -601,6 +606,7 @@ impl Item { ItemType::RawSecurity => true, ItemType::UnrealReport => true, ItemType::UserReport => true, + ItemType::ReplayEvent => true, ItemType::Session => false, ItemType::Sessions => false, ItemType::Metrics => false, diff --git a/relay-server/src/metrics_extraction/transactions.rs b/relay-server/src/metrics_extraction/transactions.rs index e44b39cda26..5b54657f619 100644 --- a/relay-server/src/metrics_extraction/transactions.rs +++ b/relay-server/src/metrics_extraction/transactions.rs @@ -296,7 +296,9 @@ fn extract_transaction_metrics_inner( event: &Event, mut push_metric: impl FnMut(Metric), ) { - if event.ty.value() != Some(&EventType::Transaction) { + if event.ty.value() != Some(&EventType::Transaction) + && event.ty.value() != Some(&EventType::ReplayEvent) + { return; } diff --git a/relay-server/src/utils/rate_limits.rs b/relay-server/src/utils/rate_limits.rs index e2988550f07..bac5f811038 100644 --- a/relay-server/src/utils/rate_limits.rs +++ b/relay-server/src/utils/rate_limits.rs @@ -105,6 +105,7 @@ fn infer_event_category(item: &Item) -> Option { ItemType::UserReport => None, ItemType::Profile => None, ItemType::ReplayPayload => None, + ItemType::ReplayEvent => None, ItemType::ClientReport => None, } } diff --git a/relay-server/src/utils/sizes.rs b/relay-server/src/utils/sizes.rs index 88ffbbfc8da..da9ea4e79b2 100644 --- a/relay-server/src/utils/sizes.rs +++ b/relay-server/src/utils/sizes.rs @@ -25,6 +25,7 @@ pub fn check_envelope_size_limits(config: &Config, envelope: &Envelope) -> bool ItemType::Event | ItemType::Transaction | ItemType::Security + | ItemType::ReplayEvent | ItemType::RawSecurity | ItemType::FormData => event_size += item.len(), ItemType::Attachment | ItemType::UnrealReport | ItemType::ReplayPayload => { diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index d534b42f20a..eebff81c694 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -22,9 +22,10 @@ outcomes_consumer, transactions_consumer, attachments_consumer, - replay_payloads_consumer, sessions_consumer, metrics_consumer, + replay_payloads_consumer, + replay_events_consumer, ) # noqa diff --git a/tests/integration/fixtures/__init__.py b/tests/integration/fixtures/__init__.py index 6948c869271..78f63996c21 100644 --- a/tests/integration/fixtures/__init__.py +++ b/tests/integration/fixtures/__init__.py @@ -166,7 +166,6 @@ def send_envelope(self, project_id, envelope, headers=None, dsn_key_idx=0): "X-Sentry-Auth": self.get_auth_header(project_id, dsn_key_idx), **(headers or {}), } - response = self.post(url, headers=headers, data=envelope.serialize()) response.raise_for_status() @@ -196,6 +195,14 @@ def send_transaction(self, project_id, payload, item_headers=None): self.send_envelope(project_id, envelope) + def send_replay_event(self, project_id, payload, item_headers=None): + envelope = Envelope() + envelope.add_item(Item(payload=PayloadRef(json=payload), type="replay_event")) + if envelope.headers is None: + envelope.headers = {} + + self.send_envelope(project_id, envelope) + def send_session_aggregates(self, project_id, payload): envelope = Envelope() envelope.add_item(Item(payload=PayloadRef(json=payload), type="sessions")) diff --git a/tests/integration/fixtures/processing.py b/tests/integration/fixtures/processing.py index 96493b61ff3..a10147ebb52 100644 --- a/tests/integration/fixtures/processing.py +++ b/tests/integration/fixtures/processing.py @@ -52,6 +52,7 @@ def inner(options=None): "sessions": get_topic_name("sessions"), "metrics": get_topic_name("metrics"), "replay_payloads": get_topic_name("replay_payloads"), + "replay_events": get_topic_name("replay_events"), } if not processing.get("redis"): @@ -286,6 +287,11 @@ def replay_payloads_consumer(kafka_consumer): return lambda: ReplayPayloadsConsumer(*kafka_consumer("replay_payloads")) +@pytest.fixture +def replay_events_consumer(kafka_consumer): + return lambda: ReplayEventsConsumer(*kafka_consumer("replay_events")) + + class MetricsConsumer(ConsumerBase): def get_metric(self, timeout=None): message = self.poll(timeout=timeout) @@ -360,3 +366,14 @@ def get_individual_replay(self): v = msgpack.unpackb(message.value(), raw=False, use_list=False) assert v["type"] == "replay_payload", v["type"] return v + + +class ReplayEventsConsumer(ConsumerBase): + def get_replay_event(self): + message = self.poll() + assert message is not None + assert message.error() is None + + event = msgpack.unpackb(message.value(), raw=False, use_list=False) + assert event["type"] == "replay_event" + return json.loads(event["payload"].decode("utf8")), event diff --git a/tests/integration/test_replay_event.py b/tests/integration/test_replay_event.py new file mode 100644 index 00000000000..a73eafa0188 --- /dev/null +++ b/tests/integration/test_replay_event.py @@ -0,0 +1,79 @@ +import pytest + +from requests.exceptions import HTTPError +from sentry_sdk.envelope import Envelope + + +def generate_replay_event(): + return { + "event_id": "d2132d31b39445f1938d7e21b6bf0ec4", + "type": "replay_event", + "transaction": "/organizations/:orgId/performance/:eventSlug/", + "start_timestamp": 1597976392.6542819, + "timestamp": 1597976400.6189718, + "contexts": { + "trace": { + "trace_id": "4C79F60C11214EB38604F4AE0781BFB2", + "span_id": "FA90FDEAD5F74052", + "type": "trace", + } + }, + "breadcrumbs": [ + { + "timestamp": 1597976410.6189718, + "category": "console", + "data": { + "arguments": ["%c first log", "more logs etc etc.", "test test",], + "logger": "console", + }, + "level": "log", + "message": "%c prev state color: #9E9E9E; font-weight: bold [object Object]", + } + ], + "spans": [ + { + "description": "", + "op": "react.mount", + "parent_span_id": "8f5a2b8768cafb4e", + "span_id": "bd429c44b67a3eb4", + "start_timestamp": 1597976393.4619668, + "timestamp": 1597976393.4718769, + "trace_id": "ff62a8b040f340bda5d830223def1d81", + } + ], + "measurements": { + "LCP": {"value": 420.69}, + " lcp_final.element-Size123 ": {"value": 1}, + "fid": {"value": 2020}, + "cls": {"value": None}, + "fp": {"value": "im a first paint"}, + "Total Blocking Time": {"value": 3.14159}, + "missing_value": "string", + }, + } + + +def test_replay_event(mini_sentry, relay_with_processing, replay_events_consumer): + relay = relay_with_processing() + mini_sentry.add_basic_project_config(42) + + replay_events_consumer = replay_events_consumer() + + replay_item = generate_replay_event() + + relay.send_replay_event(42, replay_item) + + event, _ = replay_events_consumer.get_replay_event() + assert event["transaction"] == "/organizations/:orgId/performance/:eventSlug/" + assert "trace" in event["contexts"] + assert "measurements" in event, event + assert "spans" in event, event + assert event["measurements"] == { + "lcp": {"value": 420.69}, + "lcp_final.element-size123": {"value": 1}, + "fid": {"value": 2020}, + "cls": {"value": None}, + "fp": {"value": None}, + "missing_value": None, + } + assert "breadcrumbs" in event From 39cfd118180b9dc5ff0096957d3eadad1e0da8ee Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Wed, 27 Apr 2022 09:48:55 -0700 Subject: [PATCH 04/24] pr feedback --- relay-common/src/constants.rs | 2 ++ relay-general/src/store/normalize.rs | 16 +++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/relay-common/src/constants.rs b/relay-common/src/constants.rs index 8a00171388d..0df91d8c89b 100644 --- a/relay-common/src/constants.rs +++ b/relay-common/src/constants.rs @@ -112,6 +112,8 @@ pub enum DataCategory { Session = 5, /// Replay Events ReplayEvent = 6, + /// Reserved data category that shall not appear in the outcomes. + Internal = -2, /// Any other data category not known by this Relay. #[serde(other)] Unknown = -1, diff --git a/relay-general/src/store/normalize.rs b/relay-general/src/store/normalize.rs index 9bddbc0030d..1f8cbb71dbb 100644 --- a/relay-general/src/store/normalize.rs +++ b/relay-general/src/store/normalize.rs @@ -114,10 +114,11 @@ impl<'a> NormalizeProcessor<'a> { /// Ensure measurements interface is only present for transaction events fn normalize_measurements(&self, event: &mut Event) { - if event.ty.value() != Some(&EventType::Transaction) - && event.ty.value() != Some(&EventType::ReplayEvent) - { - // Only transaction events may have a measurements interface + if !matches!( + event.ty.value(), + Some(&EventType::Transaction | &EventType::ReplayEvent) + ) { + // Only transaction/replay events may have a measurements interface event.measurements = Annotated::empty(); } } @@ -131,9 +132,10 @@ impl<'a> NormalizeProcessor<'a> { } fn normalize_spans(&self, event: &mut Event) { - if event.ty.value() == Some(&EventType::Transaction) - || event.ty.value() == Some(&EventType::ReplayEvent) - { + if matches!( + event.ty.value(), + Some(&EventType::Transaction | &EventType::ReplayEvent) + ) { spans::normalize_spans(event, &self.config.span_attributes); } } From 3d0d2ea0c70de71b989121c4f6b949fea36b02d4 Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Wed, 27 Apr 2022 09:59:11 -0700 Subject: [PATCH 05/24] fix merge --- relay-common/src/constants.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/relay-common/src/constants.rs b/relay-common/src/constants.rs index 0df91d8c89b..8a00171388d 100644 --- a/relay-common/src/constants.rs +++ b/relay-common/src/constants.rs @@ -112,8 +112,6 @@ pub enum DataCategory { Session = 5, /// Replay Events ReplayEvent = 6, - /// Reserved data category that shall not appear in the outcomes. - Internal = -2, /// Any other data category not known by this Relay. #[serde(other)] Unknown = -1, From 7915d5576b531f74e1597101c91a905456f2b9ce Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Wed, 27 Apr 2022 10:27:24 -0700 Subject: [PATCH 06/24] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e6c466744a..7765c84e09f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ - Refactor aggregation error, recover from errors more gracefully. ([#1240](https://github.com/getsentry/relay/pull/1240)) - Remove/reject nul-bytes from metric strings. ([#1235](https://github.com/getsentry/relay/pull/1235)) - Remove the unused "internal" data category. ([#1245](https://github.com/getsentry/relay/pull/1245)) -- Add ReplayRecording ItemType. ([#1236](https://github.com/getsentry/relay/pull/1236)) +- Add ReplayPayload ItemType. ([#1236](https://github.com/getsentry/relay/pull/1236)) - Add ReplayEvent ItemType. ([#1239](https://github.com/getsentry/relay/pull/1239)) ## 22.4.0 From b58a4e3d5b308d4516980b77e0987aab0df7139a Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Mon, 2 May 2022 10:29:31 -0700 Subject: [PATCH 07/24] drop events if ff not enabled --- relay-server/src/actors/envelopes.rs | 17 +++++++++++++++++ relay-server/src/actors/project.rs | 3 +++ tests/integration/test_replay_event.py | 4 +++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/relay-server/src/actors/envelopes.rs b/relay-server/src/actors/envelopes.rs index ccee0ea87f6..28d455cca46 100644 --- a/relay-server/src/actors/envelopes.rs +++ b/relay-server/src/actors/envelopes.rs @@ -970,6 +970,22 @@ impl EnvelopeProcessor { }); } + /// Remove replays if the feature flag is not enabled + fn process_replays(&self, state: &mut ProcessEnvelopeState) { + let replays_enabled = state.project_state.has_feature(Feature::Replays); + state.envelope.retain_items(|item| { + match item.ty() { + ItemType::ReplayEvent => { + if !replays_enabled { + return false; + } + true + } + _ => true, // Keep all other item types + } + }); + } + /// Creates and initializes the processing state. /// /// This applies defaults to the envelope and initializes empty rate limits. @@ -1802,6 +1818,7 @@ impl EnvelopeProcessor { self.process_client_reports(state); self.process_user_reports(state); self.process_profiles(state); + self.process_replays(state); if state.creates_event() { if_processing!({ diff --git a/relay-server/src/actors/project.rs b/relay-server/src/actors/project.rs index ed0b0638e65..f2cbd95e8c2 100644 --- a/relay-server/src/actors/project.rs +++ b/relay-server/src/actors/project.rs @@ -56,6 +56,9 @@ pub enum Feature { #[serde(rename = "organizations:profiling")] Profiling, + #[serde(rename = "organizations:session-replay")] + Replays, + /// forward compatibility #[serde(other)] Unknown, diff --git a/tests/integration/test_replay_event.py b/tests/integration/test_replay_event.py index a73eafa0188..d32838e4d50 100644 --- a/tests/integration/test_replay_event.py +++ b/tests/integration/test_replay_event.py @@ -55,7 +55,9 @@ def generate_replay_event(): def test_replay_event(mini_sentry, relay_with_processing, replay_events_consumer): relay = relay_with_processing() - mini_sentry.add_basic_project_config(42) + mini_sentry.add_basic_project_config( + 42, extra={"config": {"features": ["organizations:session-replay"]}} + ) replay_events_consumer = replay_events_consumer() From 4152676f194dc627d9d61a90872dfb0d0c602cba Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Mon, 2 May 2022 10:58:39 -0700 Subject: [PATCH 08/24] add payload to ff condition --- relay-server/src/actors/envelopes.rs | 2 +- tests/integration/test_replay_payloads.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/relay-server/src/actors/envelopes.rs b/relay-server/src/actors/envelopes.rs index abbf2fa4353..b87dcfe4d54 100644 --- a/relay-server/src/actors/envelopes.rs +++ b/relay-server/src/actors/envelopes.rs @@ -991,7 +991,7 @@ impl EnvelopeProcessor { let replays_enabled = state.project_state.has_feature(Feature::Replays); state.envelope.retain_items(|item| { match item.ty() { - ItemType::ReplayEvent => { + ItemType::ReplayEvent | ItemType::ReplayPayload => { if !replays_enabled { return false; } diff --git a/tests/integration/test_replay_payloads.py b/tests/integration/test_replay_payloads.py index 10ee4a9e879..96bf73dffac 100644 --- a/tests/integration/test_replay_payloads.py +++ b/tests/integration/test_replay_payloads.py @@ -12,7 +12,9 @@ def test_payload( event_id = "515539018c9b4260a6f999572f1661ee" relay = relay_with_processing() - mini_sentry.add_full_project_config(project_id) + mini_sentry.add_full_project_config( + project_id, extra={"config": {"features": ["organizations:session-replay"]}} + ) replay_payloads_consumer = replay_payloads_consumer() outcomes_consumer = outcomes_consumer() From 45d0531fecc0822bd475e8dc5a3614c4a6992eac Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Mon, 2 May 2022 11:10:26 -0700 Subject: [PATCH 09/24] fix itemtype reference --- relay-server/src/actors/store.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/relay-server/src/actors/store.rs b/relay-server/src/actors/store.rs index 9fcd4ece53e..c17e55ed13c 100644 --- a/relay-server/src/actors/store.rs +++ b/relay-server/src/actors/store.rs @@ -701,7 +701,7 @@ fn is_slow_item(item: &Item) -> bool { } fn is_replay_payload(item: &Item) -> bool { - item.ty() == ItemType::ReplayPayload + item.ty() == &ItemType::ReplayPayload } impl Handler for StoreForwarder { From 874051ce302bdfe33210d2eb675edb2471f4b4c7 Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Mon, 2 May 2022 17:56:08 -0700 Subject: [PATCH 10/24] fix itemtype reference --- relay-server/src/actors/store.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/relay-server/src/actors/store.rs b/relay-server/src/actors/store.rs index faaaf029263..10b80655de1 100644 --- a/relay-server/src/actors/store.rs +++ b/relay-server/src/actors/store.rs @@ -744,7 +744,7 @@ fn is_slow_item(item: &Item) -> bool { } fn is_replay_payload(item: &Item) -> bool { - item.ty() == ItemType::ReplayPayload + item.ty() == &ItemType::ReplayPayload } impl Handler for StoreForwarder { From dc2789291092e23356431d91730fe5e73eda6e65 Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Mon, 2 May 2022 18:01:27 -0700 Subject: [PATCH 11/24] add replay items to from_str method --- relay-server/src/envelope.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/relay-server/src/envelope.rs b/relay-server/src/envelope.rs index d50d90766d0..09568160078 100644 --- a/relay-server/src/envelope.rs +++ b/relay-server/src/envelope.rs @@ -173,6 +173,8 @@ impl std::str::FromStr for ItemType { "metric_buckets" => Self::MetricBuckets, "client_report" => Self::ClientReport, "profile" => Self::Profile, + "replay_payload" => Self::ReplayPayload, + "replay_event" => Self::ReplayEvent, other => Self::Unknown(other.to_owned()), }) } From 55248e8de91853b158564123e4ea9824fd8d09e7 Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Wed, 11 May 2022 17:01:42 -0700 Subject: [PATCH 12/24] remove ReplayEvent EventType --- relay-common/src/constants.rs | 4 - relay-general/src/store/normalize.rs | 13 +- .../test_fixtures__event_schema.snap.new | 2891 +++++++++++++++++ relay-server/src/actors/envelopes.rs | 7 - relay-server/src/envelope.rs | 3 +- .../src/metrics_extraction/transactions.rs | 4 +- tests/integration/test_replay_event.py | 13 +- 7 files changed, 2898 insertions(+), 37 deletions(-) create mode 100644 relay-general/tests/snapshots/test_fixtures__event_schema.snap.new diff --git a/relay-common/src/constants.rs b/relay-common/src/constants.rs index 6524c1b22c7..01bfa491f78 100644 --- a/relay-common/src/constants.rs +++ b/relay-common/src/constants.rs @@ -36,8 +36,6 @@ pub enum EventType { ExpectStaple, /// Performance monitoring transactions carrying spans. Transaction, - /// Replay Events for session replays - ReplayEvent, /// All events that do not qualify as any other type. #[serde(other)] Default, @@ -88,7 +86,6 @@ impl fmt::Display for EventType { EventType::ExpectCt => write!(f, "expectct"), EventType::ExpectStaple => write!(f, "expectstaple"), EventType::Transaction => write!(f, "transaction"), - EventType::ReplayEvent => write!(f, "replay_event"), } } } @@ -182,7 +179,6 @@ impl From for DataCategory { match ty { EventType::Default | EventType::Error => Self::Error, EventType::Transaction => Self::Transaction, - EventType::ReplayEvent => Self::ReplayEvent, EventType::Csp | EventType::Hpkp | EventType::ExpectCt | EventType::ExpectStaple => { Self::Security } diff --git a/relay-general/src/store/normalize.rs b/relay-general/src/store/normalize.rs index e46d50fd239..7263fe4cfcb 100644 --- a/relay-general/src/store/normalize.rs +++ b/relay-general/src/store/normalize.rs @@ -114,10 +114,7 @@ impl<'a> NormalizeProcessor<'a> { /// Ensure measurements interface is only present for transaction events fn normalize_measurements(&self, event: &mut Event) { - if !matches!( - event.ty.value(), - Some(&EventType::Transaction | &EventType::ReplayEvent) - ) { + if event.ty.value() != Some(&EventType::Transaction) { // Only transaction/replay events may have a measurements interface event.measurements = Annotated::empty(); } @@ -132,10 +129,7 @@ impl<'a> NormalizeProcessor<'a> { } fn normalize_spans(&self, event: &mut Event) { - if matches!( - event.ty.value(), - Some(&EventType::Transaction | &EventType::ReplayEvent) - ) { + if event.ty.value() != Some(&EventType::Transaction) { spans::normalize_spans(event, &self.config.span_attributes); } } @@ -266,9 +260,6 @@ impl<'a> NormalizeProcessor<'a> { if event.ty.value() == Some(&EventType::Transaction) { return EventType::Transaction; } - if event.ty.value() == Some(&EventType::ReplayEvent) { - return EventType::ReplayEvent; - } // The SDKs do not describe event types, and we must infer them from available attributes. let has_exceptions = event diff --git a/relay-general/tests/snapshots/test_fixtures__event_schema.snap.new b/relay-general/tests/snapshots/test_fixtures__event_schema.snap.new new file mode 100644 index 00000000000..122693084e4 --- /dev/null +++ b/relay-general/tests/snapshots/test_fixtures__event_schema.snap.new @@ -0,0 +1,2891 @@ +--- +source: relay-general/tests/test_fixtures.rs +expression: event_json_schema() +--- +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Event", + "description": " The sentry v7 event structure.", + "anyOf": [ + { + "type": "object", + "properties": { + "breadcrumbs": { + "description": " List of breadcrumbs recorded before this event.", + "default": null, + "type": [ + "object", + "null" + ], + "required": [ + "values" + ], + "properties": { + "values": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/Breadcrumb" + }, + { + "type": "null" + } + ] + } + } + } + }, + "contexts": { + "description": " Contexts describing the environment (e.g. device, os or browser).", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Contexts" + }, + { + "type": "null" + } + ] + }, + "culprit": { + "description": " Custom culprit of the event.\n\n This field is deprecated and shall not be set by client SDKs.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "debug_meta": { + "description": " Meta data for event processing and debugging.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/DebugMeta" + }, + { + "type": "null" + } + ] + }, + "dist": { + "description": " Program's distribution identifier.\n\n The distribution of the application.\n\n Distributions are used to disambiguate build or deployment variants of the same release of\n an application. For example, the dist can be the build number of an XCode build or the\n version code of an Android build.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "environment": { + "description": " The environment name, such as `production` or `staging`.\n\n ```json\n { \"environment\": \"production\" }\n ```", + "default": null, + "type": [ + "string", + "null" + ] + }, + "errors": { + "description": " Errors encountered during processing. Intended to be phased out in favor of\n annotation/metadata system.", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "anyOf": [ + { + "$ref": "#/definitions/EventProcessingError" + }, + { + "type": "null" + } + ] + } + }, + "event_id": { + "description": " Unique identifier of this event.\n\n Hexadecimal string representing a uuid4 value. The length is exactly 32 characters. Dashes\n are not allowed. Has to be lowercase.\n\n Even though this field is backfilled on the server with a new uuid4, it is strongly\n recommended to generate that uuid4 clientside. There are some features like user feedback\n which are easier to implement that way, and debugging in case events get lost in your\n Sentry installation is also easier.\n\n Example:\n\n ```json\n {\n \"event_id\": \"fc6d8c0c43fc4630ad850ee518f1b9d0\"\n }\n ```", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/EventId" + }, + { + "type": "null" + } + ] + }, + "exception": { + "description": " One or multiple chained (nested) exceptions.", + "default": null, + "type": [ + "object", + "null" + ], + "required": [ + "values" + ], + "properties": { + "values": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/Exception" + }, + { + "type": "null" + } + ] + } + } + } + }, + "extra": { + "description": " Arbitrary extra information set by the user.\n\n ```json\n {\n \"extra\": {\n \"my_key\": 1,\n \"some_other_value\": \"foo bar\"\n }\n }```", + "default": null, + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "fingerprint": { + "description": " Manual fingerprint override.\n\n A list of strings used to dictate how this event is supposed to be grouped with other\n events into issues. For more information about overriding grouping see [Customize Grouping\n with Fingerprints](https://docs.sentry.io/data-management/event-grouping/).\n\n ```json\n {\n \"fingerprint\": [\"myrpc\", \"POST\", \"/foo.bar\"]\n }", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Fingerprint" + }, + { + "type": "null" + } + ] + }, + "level": { + "description": " Severity level of the event. Defaults to `error`.\n\n Example:\n\n ```json\n {\"level\": \"warning\"}\n ```", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Level" + }, + { + "type": "null" + } + ] + }, + "logentry": { + "description": " Custom parameterized message for this event.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/LogEntry" + }, + { + "type": "null" + } + ] + }, + "logger": { + "description": " Logger that created the event.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "modules": { + "description": " Name and versions of all installed modules/packages/dependencies in the current\n environment/application.\n\n ```json\n { \"django\": \"3.0.0\", \"celery\": \"4.2.1\" }\n ```\n\n In Python this is a list of installed packages as reported by `pkg_resources` together with\n their reported version string.\n\n This is primarily used for suggesting to enable certain SDK integrations from within the UI\n and for making informed decisions on which frameworks to support in future development\n efforts.", + "default": null, + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": [ + "string", + "null" + ] + } + }, + "platform": { + "description": " Platform identifier of this event (defaults to \"other\").\n\n A string representing the platform the SDK is submitting from. This will be used by the\n Sentry interface to customize various components in the interface, but also to enter or\n skip stacktrace processing.\n\n Acceptable values are: `as3`, `c`, `cfml`, `cocoa`, `csharp`, `elixir`, `haskell`, `go`,\n `groovy`, `java`, `javascript`, `native`, `node`, `objc`, `other`, `perl`, `php`, `python`,\n `ruby`", + "default": null, + "type": [ + "string", + "null" + ] + }, + "received": { + "description": " Timestamp when the event has been received by Sentry.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Timestamp" + }, + { + "type": "null" + } + ] + }, + "release": { + "description": " The release version of the application.\n\n **Release versions must be unique across all projects in your organization.** This value\n can be the git SHA for the given project, or a product identifier with a semantic version.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "request": { + "description": " Information about a web request that occurred during the event.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Request" + }, + { + "type": "null" + } + ] + }, + "sdk": { + "description": " Information about the Sentry SDK that generated this event.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/ClientSdkInfo" + }, + { + "type": "null" + } + ] + }, + "server_name": { + "description": " Server or device name the event was generated on.\n\n This is supposed to be a hostname.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "stacktrace": { + "description": " Event stacktrace.\n\n DEPRECATED: Prefer `threads` or `exception` depending on which is more appropriate.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Stacktrace" + }, + { + "type": "null" + } + ] + }, + "tags": { + "description": " Custom tags for this event.\n\n A map or list of tags for this event. Each tag must be less than 200 characters.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Tags" + }, + { + "type": "null" + } + ] + }, + "threads": { + "description": " Threads that were active when the event occurred.", + "default": null, + "type": [ + "object", + "null" + ], + "required": [ + "values" + ], + "properties": { + "values": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/Thread" + }, + { + "type": "null" + } + ] + } + } + } + }, + "time_spent": { + "description": " Time since the start of the transaction until the error occurred.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "timestamp": { + "description": " Timestamp when the event was created.\n\n Indicates when the event was created in the Sentry SDK. The format is either a string as\n defined in [RFC 3339](https://tools.ietf.org/html/rfc3339) or a numeric (integer or float)\n value representing the number of seconds that have elapsed since the [Unix\n epoch](https://en.wikipedia.org/wiki/Unix_time).\n\n Timezone is assumed to be UTC if missing.\n\n Sub-microsecond precision is not preserved with numeric values due to precision\n limitations with floats (at least in our systems). With that caveat in mind, just send\n whatever is easiest to produce.\n\n All timestamps in the event protocol are formatted this way.\n\n # Example\n\n All of these are the same date:\n\n ```json\n { \"timestamp\": \"2011-05-02T17:41:36Z\" }\n { \"timestamp\": \"2011-05-02T17:41:36\" }\n { \"timestamp\": \"2011-05-02T17:41:36.000\" }\n { \"timestamp\": 1304358096.0 }\n ```", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Timestamp" + }, + { + "type": "null" + } + ] + }, + "transaction": { + "description": " Transaction name of the event.\n\n For example, in a web app, this might be the route name (`\"/users//\"` or\n `UserView`), in a task queue it might be the function + module name.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "type": { + "description": " Type of the event. Defaults to `default`.\n\n The event type determines how Sentry handles the event and has an impact on processing, rate\n limiting, and quotas. There are three fundamental classes of event types:\n\n - **Error monitoring events**: Processed and grouped into unique issues based on their\n exception stack traces and error messages.\n - **Security events**: Derived from Browser security violation reports and grouped into\n unique issues based on the endpoint and violation. SDKs do not send such events.\n - **Transaction events** (`transaction`): Contain operation spans and collected into traces\n for performance monitoring.\n\n Transactions must explicitly specify the `\"transaction\"` event type. In all other cases,\n Sentry infers the appropriate event type from the payload and overrides the stated type.\n SDKs should not send an event type other than for transactions.\n\n Example:\n\n ```json\n {\n \"type\": \"transaction\",\n \"spans\": []\n }\n ```", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/EventType" + }, + { + "type": "null" + } + ] + }, + "user": { + "description": " Information about the user who triggered this event.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/User" + }, + { + "type": "null" + } + ] + }, + "version": { + "description": " Version", + "default": null, + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Addr": { + "type": "string" + }, + "AppContext": { + "description": " Application information.\n\n App context describes the application. As opposed to the runtime, this is the actual\n application that was running and carries metadata about the current session.", + "anyOf": [ + { + "type": "object", + "properties": { + "app_build": { + "description": " Internal build ID as it appears on the platform.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "app_identifier": { + "description": " Version-independent application identifier, often a dotted bundle ID.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "app_name": { + "description": " Application name as it appears on the platform.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "app_start_time": { + "description": " Start time of the app.\n\n Formatted UTC timestamp when the user started the application.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "app_version": { + "description": " Application version as it appears on the platform.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "build_type": { + "description": " String identifying the kind of build. For example, `testflight`.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "device_app_hash": { + "description": " Application-specific device identifier.", + "default": null, + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + ] + }, + "AppleDebugImage": { + "description": " Legacy apple debug images (MachO).\n\n This was also used for non-apple platforms with similar debug setups.", + "anyOf": [ + { + "type": "object", + "required": [ + "image_addr", + "image_size", + "name", + "uuid" + ], + "properties": { + "arch": { + "description": " CPU architecture target.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "cpu_subtype": { + "description": " MachO CPU subtype identifier.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "cpu_type": { + "description": " MachO CPU type identifier.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "image_addr": { + "description": " Starting memory address of the image (required).", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "image_size": { + "description": " Size of the image in bytes (required).", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "image_vmaddr": { + "description": " Loading address in virtual memory.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "name": { + "description": " Path and name of the debug image (required).", + "type": [ + "string", + "null" + ] + }, + "uuid": { + "description": " The unique UUID of the image.", + "type": [ + "string", + "null" + ], + "format": "uuid" + } + }, + "additionalProperties": false + } + ] + }, + "Breadcrumb": { + "description": " The Breadcrumbs Interface specifies a series of application events, or \"breadcrumbs\", that\n occurred before an event.\n\n An event may contain one or more breadcrumbs in an attribute named `breadcrumbs`. The entries\n are ordered from oldest to newest. Consequently, the last entry in the list should be the last\n entry before the event occurred.\n\n While breadcrumb attributes are not strictly validated in Sentry, a breadcrumb is most useful\n when it includes at least a `timestamp` and `type`, `category` or `message`. The rendering of\n breadcrumbs in Sentry depends on what is provided.\n\n The following example illustrates the breadcrumbs part of the event payload and omits other\n attributes for simplicity.\n\n ```json\n {\n \"breadcrumbs\": {\n \"values\": [\n {\n \"timestamp\": \"2016-04-20T20:55:53.845Z\",\n \"message\": \"Something happened\",\n \"category\": \"log\",\n \"data\": {\n \"foo\": \"bar\",\n \"blub\": \"blah\"\n }\n },\n {\n \"timestamp\": \"2016-04-20T20:55:53.847Z\",\n \"type\": \"navigation\",\n \"data\": {\n \"from\": \"/login\",\n \"to\": \"/dashboard\"\n }\n }\n ]\n }\n }\n ```", + "anyOf": [ + { + "type": "object", + "properties": { + "category": { + "description": " A dotted string indicating what the crumb is or from where it comes. _Optional._\n\n Typically it is a module name or a descriptive string. For instance, _ui.click_ could be\n used to indicate that a click happened in the UI or _flask_ could be used to indicate that\n the event originated in the Flask framework.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "data": { + "description": " Arbitrary data associated with this breadcrumb.\n\n Contains a dictionary whose contents depend on the breadcrumb `type`. Additional parameters\n that are unsupported by the type are rendered as a key/value table.", + "default": null, + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "event_id": { + "description": " Identifier of the event this breadcrumb belongs to.\n\n Sentry events can appear as breadcrumbs in other events as long as they have occurred in the\n same organization. This identifier links to the original event.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/EventId" + }, + { + "type": "null" + } + ] + }, + "level": { + "description": " Severity level of the breadcrumb. _Optional._\n\n Allowed values are, from highest to lowest: `fatal`, `error`, `warning`, `info`, and\n `debug`. Levels are used in the UI to emphasize and deemphasize the crumb. Defaults to\n `info`.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Level" + }, + { + "type": "null" + } + ] + }, + "message": { + "description": " Human readable message for the breadcrumb.\n\n If a message is provided, it is rendered as text with all whitespace preserved. Very long\n text might be truncated in the UI.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "timestamp": { + "description": " The timestamp of the breadcrumb. Recommended.\n\n A timestamp representing when the breadcrumb occurred. The format is either a string as\n defined in [RFC 3339](https://tools.ietf.org/html/rfc3339) or a numeric (integer or float)\n value representing the number of seconds that have elapsed since the [Unix\n epoch](https://en.wikipedia.org/wiki/Unix_time).\n\n Breadcrumbs are most useful when they include a timestamp, as it creates a timeline leading\n up to an event.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Timestamp" + }, + { + "type": "null" + } + ] + }, + "type": { + "description": " The type of the breadcrumb. _Optional_, defaults to `default`.\n\n - `default`: Describes a generic breadcrumb. This is typically a log message or\n user-generated breadcrumb. The `data` field is entirely undefined and as such, completely\n rendered as a key/value table.\n\n - `navigation`: Describes a navigation breadcrumb. A navigation event can be a URL change\n in a web application, or a UI transition in a mobile or desktop application, etc.\n\n Such a breadcrumb's `data` object has the required fields `from` and `to`, which\n represent an application route/url each.\n\n - `http`: Describes an HTTP request breadcrumb. This represents an HTTP request transmitted\n from your application. This could be an AJAX request from a web application, or a\n server-to-server HTTP request to an API service provider, etc.\n\n Such a breadcrumb's `data` property has the fields `url`, `method`, `status_code`\n (integer) and `reason` (string).", + "default": null, + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + ] + }, + "BrowserContext": { + "description": " Web browser information.", + "anyOf": [ + { + "type": "object", + "properties": { + "name": { + "description": " Display name of the browser application.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "version": { + "description": " Version string of the browser.", + "default": null, + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + ] + }, + "CError": { + "description": " POSIX signal with optional extended data.\n\n Error codes set by Linux system calls and some library functions as specified in ISO C99,\n POSIX.1-2001, and POSIX.1-2008. See\n [`errno(3)`](https://man7.org/linux/man-pages/man3/errno.3.html) for more information.", + "anyOf": [ + { + "type": "object", + "properties": { + "name": { + "description": " Optional name of the errno constant.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "number": { + "description": " The error code as specified by ISO C99, POSIX.1-2001 or POSIX.1-2008.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "int64" + } + }, + "additionalProperties": false + } + ] + }, + "ClientSdkInfo": { + "description": " The SDK Interface describes the Sentry SDK and its configuration used to capture and transmit an event.", + "anyOf": [ + { + "type": "object", + "required": [ + "name", + "version" + ], + "properties": { + "integrations": { + "description": " List of integrations that are enabled in the SDK. _Optional._\n\n The list should have all enabled integrations, including default integrations. Default\n integrations are included because different SDK releases may contain different default\n integrations.", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + }, + "name": { + "description": " Unique SDK name. _Required._\n\n The name of the SDK. The format is `entity.ecosystem[.flavor]` where entity identifies the\n developer of the SDK, ecosystem refers to the programming language or platform where the\n SDK is to be used and the optional flavor is used to identify standalone SDKs that are part\n of a major ecosystem.\n\n Official Sentry SDKs use the entity `sentry`, as in `sentry.python` or\n `sentry.javascript.react-native`. Please use a different entity for your own SDKs.", + "type": [ + "string", + "null" + ] + }, + "packages": { + "description": " List of installed and loaded SDK packages. _Optional._\n\n A list of packages that were installed as part of this SDK or the activated integrations.\n Each package consists of a name in the format `source:identifier` and `version`. If the\n source is a Git repository, the `source` should be `git`, the identifier should be a\n checkout link and the version should be a Git reference (branch, tag or SHA).", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "anyOf": [ + { + "$ref": "#/definitions/ClientSdkPackage" + }, + { + "type": "null" + } + ] + } + }, + "version": { + "description": " The version of the SDK. _Required._\n\n It should have the [Semantic Versioning](https://semver.org/) format `MAJOR.MINOR.PATCH`,\n without any prefix (no `v` or anything else in front of the major version number).\n\n Examples: `0.1.0`, `1.0.0`, `4.3.12`", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + ] + }, + "ClientSdkPackage": { + "description": " An installed and loaded package as part of the Sentry SDK.", + "anyOf": [ + { + "type": "object", + "properties": { + "name": { + "description": " Name of the package.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "version": { + "description": " Version of the package.", + "default": null, + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + ] + }, + "CodeId": { + "type": "string" + }, + "Context": { + "description": " A context describes environment info (e.g. device, os or browser).", + "anyOf": [ + { + "$ref": "#/definitions/DeviceContext" + }, + { + "$ref": "#/definitions/OsContext" + }, + { + "$ref": "#/definitions/RuntimeContext" + }, + { + "$ref": "#/definitions/AppContext" + }, + { + "$ref": "#/definitions/BrowserContext" + }, + { + "$ref": "#/definitions/GpuContext" + }, + { + "$ref": "#/definitions/TraceContext" + }, + { + "$ref": "#/definitions/MonitorContext" + }, + { + "type": "object", + "additionalProperties": true + } + ] + }, + "ContextInner": { + "anyOf": [ + { + "$ref": "#/definitions/Context" + } + ] + }, + "Contexts": { + "description": " The Contexts Interface provides additional context data. Typically, this is data related to the\n current user and the environment. For example, the device or application version. Its canonical\n name is `contexts`.\n\n The `contexts` type can be used to define arbitrary contextual data on the event. It accepts an\n object of key/value pairs. The key is the “alias” of the context and can be freely chosen.\n However, as per policy, it should match the type of the context unless there are two values for\n a type. You can omit `type` if the key name is the type.\n\n Unknown data for the contexts is rendered as a key/value list.\n\n For more details about sending additional data with your event, see the [full documentation on\n Additional Data](https://docs.sentry.io/enriching-error-data/additional-data/).", + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/definitions/ContextInner" + }, + { + "type": "null" + } + ] + } + } + ] + }, + "Cookies": { + "description": " A map holding cookies.", + "anyOf": [ + { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": [ + "string", + "null" + ] + } + }, + { + "type": "array", + "items": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "string", + "null" + ] + } + ], + "maxItems": 2, + "minItems": 2 + } + } + ] + } + ] + }, + "DebugId": { + "type": "string" + }, + "DebugImage": { + "description": " A debug information file (debug image).", + "anyOf": [ + { + "$ref": "#/definitions/AppleDebugImage" + }, + { + "$ref": "#/definitions/NativeDebugImage" + }, + { + "$ref": "#/definitions/NativeDebugImage" + }, + { + "$ref": "#/definitions/NativeDebugImage" + }, + { + "$ref": "#/definitions/NativeDebugImage" + }, + { + "$ref": "#/definitions/ProguardDebugImage" + }, + { + "$ref": "#/definitions/NativeDebugImage" + }, + { + "type": "object", + "additionalProperties": true + } + ] + }, + "DebugMeta": { + "description": " Debugging and processing meta information.\n\n The debug meta interface carries debug information for processing errors and crash reports.\n Sentry amends the information in this interface.\n\n Example (look at field types to see more detail):\n\n ```json\n {\n \"debug_meta\": {\n \"images\": [],\n \"sdk_info\": {\n \"sdk_name\": \"iOS\",\n \"version_major\": 10,\n \"version_minor\": 3,\n \"version_patchlevel\": 0\n }\n }\n }\n ```", + "anyOf": [ + { + "type": "object", + "properties": { + "images": { + "description": " List of debug information files (debug images).", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "anyOf": [ + { + "$ref": "#/definitions/DebugImage" + }, + { + "type": "null" + } + ] + } + }, + "sdk_info": { + "description": " Information about the system SDK (e.g. iOS SDK).", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/SystemSdkInfo" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + ] + }, + "DeviceContext": { + "description": " Device information.\n\n Device context describes the device that caused the event. This is most appropriate for mobile\n applications.", + "anyOf": [ + { + "type": "object", + "properties": { + "arch": { + "description": " Native cpu architecture of the device.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "battery_level": { + "description": " Current battery level in %.\n\n If the device has a battery, this can be a floating point value defining the battery level\n (in the range 0-100).", + "default": null, + "type": [ + "number", + "null" + ], + "format": "double" + }, + "boot_time": { + "description": " Indicator when the device was booted.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "brand": { + "description": " Brand of the device.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "charging": { + "description": " Whether the device was charging or not.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "external_free_storage": { + "description": " Free size of the attached external storage in bytes (eg: android SDK card).", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "external_storage_size": { + "description": " Total size of the attached external storage in bytes (eg: android SDK card).", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "family": { + "description": " Family of the device model.\n\n This is usually the common part of model names across generations. For instance, `iPhone`\n would be a reasonable family, so would be `Samsung Galaxy`.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "free_memory": { + "description": " How much memory is still available in bytes.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "free_storage": { + "description": " How much storage is free in bytes.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "low_memory": { + "description": " Whether the device was low on memory.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "manufacturer": { + "description": " Manufacturer of the device.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "memory_size": { + "description": " Total memory available in bytes.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "model": { + "description": " Device model.\n\n This, for example, can be `Samsung Galaxy S3`.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "model_id": { + "description": " Device model (internal identifier).\n\n An internal hardware revision to identify the device exactly.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "name": { + "description": " Name of the device.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "online": { + "description": " Whether the device was online or not.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "orientation": { + "description": " Current screen orientation.\n\n This can be a string `portrait` or `landscape` to define the orientation of a device.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "screen_density": { + "description": " Device screen density.", + "default": null, + "type": [ + "number", + "null" + ], + "format": "double" + }, + "screen_dpi": { + "description": " Screen density as dots-per-inch.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "screen_resolution": { + "description": " Device screen resolution.\n\n (e.g.: 800x600, 3040x1444)", + "default": null, + "type": [ + "string", + "null" + ] + }, + "simulator": { + "description": " Simulator/prod indicator.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "storage_size": { + "description": " Total storage size of the device in bytes.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "timezone": { + "description": " Timezone of the device.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "usable_memory": { + "description": " How much memory is usable for the app in bytes.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "EventId": { + "description": " Wrapper around a UUID with slightly different formatting.", + "anyOf": [ + { + "type": "string", + "format": "uuid" + } + ] + }, + "EventProcessingError": { + "description": " An event processing error.", + "anyOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "name": { + "description": " Affected key or deep path.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "type": { + "description": " The error kind.", + "type": [ + "string", + "null" + ] + }, + "value": { + "description": " The original value causing this error.", + "default": null + } + }, + "additionalProperties": false + } + ] + }, + "EventType": { + "description": "The type of an event.\n\nThe event type determines how Sentry handles the event and has an impact on processing, rate limiting, and quotas. There are three fundamental classes of event types:\n\n- **Error monitoring events** (`default`, `error`): Processed and grouped into unique issues based on their exception stack traces and error messages. - **Security events** (`csp`, `hpkp`, `expectct`, `expectstaple`): Derived from Browser security violation reports and grouped into unique issues based on the endpoint and violation. SDKs do not send such events. - **Transaction events** (`transaction`): Contain operation spans and collected into traces for performance monitoring.", + "type": "string", + "enum": [ + "error", + "csp", + "hpkp", + "expectct", + "expectstaple", + "transaction", + "default" + ] + }, + "Exception": { + "description": " A single exception.\n\n Multiple values inside of an [event](#typedef-Event) represent chained exceptions and should be sorted oldest to newest. For example, consider this Python code snippet:\n\n ```python\n try:\n raise Exception(\"random boring invariant was not met!\")\n except Exception as e:\n raise ValueError(\"something went wrong, help!\") from e\n ```\n\n `Exception` would be described first in the values list, followed by a description of `ValueError`:\n\n ```json\n {\n \"exception\": {\n \"values\": [\n {\"type\": \"Exception\": \"value\": \"random boring invariant was not met!\"},\n {\"type\": \"ValueError\", \"value\": \"something went wrong, help!\"},\n ]\n }\n }\n ```", + "anyOf": [ + { + "type": "object", + "properties": { + "mechanism": { + "description": " Mechanism by which this exception was generated and handled.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Mechanism" + }, + { + "type": "null" + } + ] + }, + "module": { + "description": " The optional module, or package which the exception type lives in.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "stacktrace": { + "description": " Stack trace containing frames of this exception.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Stacktrace" + }, + { + "type": "null" + } + ] + }, + "thread_id": { + "description": " An optional value that refers to a [thread](#typedef-Thread).", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/ThreadId" + }, + { + "type": "null" + } + ] + }, + "type": { + "description": " Exception type, e.g. `ValueError`.\n\n At least one of `type` or `value` is required, otherwise the exception is discarded.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "value": { + "description": " Human readable display value.\n\n At least one of `type` or `value` is required, otherwise the exception is discarded.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/JsonLenientString" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + ] + }, + "Fingerprint": { + "description": " A fingerprint value.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "Frame": { + "description": " Holds information about a single stacktrace frame.\n\n Each object should contain **at least** a `filename`, `function` or `instruction_addr` attribute. All values are optional, but recommended.", + "anyOf": [ + { + "type": "object", + "properties": { + "abs_path": { + "description": " Absolute path to the source file.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/NativeImagePath" + }, + { + "type": "null" + } + ] + }, + "addr_mode": { + "description": " Defines the addressing mode for addresses.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "colno": { + "description": " Column number within the source file, starting at 1.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "context_line": { + "description": " Source code of the current line (`lineno`).", + "default": null, + "type": [ + "string", + "null" + ] + }, + "filename": { + "description": " The source file name (basename only).", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/NativeImagePath" + }, + { + "type": "null" + } + ] + }, + "function": { + "description": " Name of the frame's function. This might include the name of a class.\n\n This function name may be shortened or demangled. If not, Sentry will demangle and shorten\n it for some platforms. The original function name will be stored in `raw_function`.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "image_addr": { + "description": " (C/C++/Native) Start address of the containing code module (image).", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "in_app": { + "description": " Override whether this frame should be considered part of application code, or part of\n libraries/frameworks/dependencies.\n\n Setting this attribute to `false` causes the frame to be hidden/collapsed by default and\n mostly ignored during issue grouping.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "instruction_addr": { + "description": " (C/C++/Native) An optional instruction address for symbolication.\n\n This should be a string with a hexadecimal number that includes a 0x prefix.\n If this is set and a known image is defined in the\n [Debug Meta Interface]({%- link _documentation/development/sdk-dev/event-payloads/debugmeta.md -%}),\n then symbolication can take place.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "lineno": { + "description": " Line number within the source file, starting at 1.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "module": { + "description": " Name of the module the frame is contained in.\n\n Note that this might also include a class name if that is something the\n language natively considers to be part of the stack (for instance in Java).", + "default": null, + "type": [ + "string", + "null" + ] + }, + "package": { + "description": " Name of the package that contains the frame.\n\n For instance this can be a dylib for native languages, the name of the jar\n or .NET assembly.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "platform": { + "description": " Which platform this frame is from.\n\n This can override the platform for a single frame. Otherwise, the platform of the event is\n assumed. This can be used for multi-platform stack traces, such as in React Native.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "post_context": { + "description": " Source code of the lines after `lineno`.", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + }, + "pre_context": { + "description": " Source code leading up to `lineno`.", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + }, + "raw_function": { + "description": " A raw (but potentially truncated) function value.\n\n The original function name, if the function name is shortened or demangled. Sentry shows\n the raw function when clicking on the shortened one in the UI.\n\n If this has the same value as `function` it's best to be omitted. This\n exists because on many platforms the function itself contains additional\n information like overload specifies or a lot of generics which can make\n it exceed the maximum limit we provide for the field. In those cases\n then we cannot reliably trim down the function any more at a later point\n because the more valuable information has been removed.\n\n The logic to be applied is that an intelligently trimmed function name\n should be stored in `function` and the value before trimming is stored\n in this field instead. However also this field will be capped at 256\n characters at the moment which often means that not the entire original\n value can be stored.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "stack_start": { + "description": " Marks this frame as the bottom of a chained stack trace.\n\n Stack traces from asynchronous code consist of several sub traces that are chained together\n into one large list. This flag indicates the root function of a chained stack trace.\n Depending on the runtime and thread, this is either the `main` function or a thread base\n stub.\n\n This field should only be specified when true.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "symbol": { + "description": " Potentially mangled name of the symbol as it appears in an executable.\n\n This is different from a function name by generally being the mangled\n name that appears natively in the binary. This is relevant for languages\n like Swift, C++ or Rust.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "symbol_addr": { + "description": " (C/C++/Native) Start address of the frame's function.\n\n We use the instruction address for symbolication, but this can be used to calculate\n an instruction offset automatically.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "vars": { + "description": " Mapping of local variables and expression names that were available in this frame.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/FrameVars" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + ] + }, + "FrameVars": { + "description": " Frame local variables.", + "anyOf": [ + { + "type": "object", + "additionalProperties": true + } + ] + }, + "Geo": { + "description": " Geographical location of the end user or device.", + "anyOf": [ + { + "type": "object", + "properties": { + "city": { + "description": " Human readable city name.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "country_code": { + "description": " Two-letter country code (ISO 3166-1 alpha-2).", + "default": null, + "type": [ + "string", + "null" + ] + }, + "region": { + "description": " Human readable region name or code.", + "default": null, + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + ] + }, + "GpuContext": { + "description": " GPU information.\n\n Example:\n\n ```json\n \"gpu\": {\n \"name\": \"AMD Radeon Pro 560\",\n \"vendor_name\": \"Apple\",\n \"memory_size\": 4096,\n \"api_type\": \"Metal\",\n \"multi_threaded_rendering\": true,\n \"version\": \"Metal\",\n \"npot_support\": \"Full\"\n }\n ```", + "anyOf": [ + { + "type": "object", + "properties": { + "api_type": { + "description": " The device low-level API type.\n\n Examples: `\"Apple Metal\"` or `\"Direct3D11\"`", + "default": null, + "type": [ + "string", + "null" + ] + }, + "id": { + "description": " The PCI identifier of the graphics device.", + "default": null + }, + "memory_size": { + "description": " The total GPU memory available in Megabytes.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "multi_threaded_rendering": { + "description": " Whether the GPU has multi-threaded rendering or not.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "name": { + "description": " The name of the graphics device.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "npot_support": { + "description": " The Non-Power-Of-Two support.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "vendor_id": { + "description": " The PCI vendor identifier of the graphics device.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "vendor_name": { + "description": " The vendor name as reported by the graphics device.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "version": { + "description": " The Version of the graphics device.", + "default": null, + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + ] + }, + "HeaderName": { + "description": " A \"into-string\" type that normalizes header names.", + "anyOf": [ + { + "type": "string" + } + ] + }, + "HeaderValue": { + "description": " A \"into-string\" type that normalizes header values.", + "anyOf": [ + { + "type": "string" + } + ] + }, + "Headers": { + "description": " A map holding headers.", + "anyOf": [ + { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/definitions/HeaderValue" + }, + { + "type": "null" + } + ] + } + }, + { + "type": "array", + "items": { + "type": [ + "array", + "null" + ], + "items": [ + { + "anyOf": [ + { + "$ref": "#/definitions/HeaderName" + }, + { + "type": "null" + } + ] + }, + { + "anyOf": [ + { + "$ref": "#/definitions/HeaderValue" + }, + { + "type": "null" + } + ] + } + ], + "maxItems": 2, + "minItems": 2 + } + } + ] + } + ] + }, + "JsonLenientString": { + "description": " A \"into-string\" type of value. All non-string values are serialized as JSON.", + "anyOf": [ + { + "type": "string" + } + ] + }, + "Level": { + "description": "Severity level of an event or breadcrumb.", + "type": "string", + "enum": [ + "debug", + "info", + "warning", + "error", + "fatal" + ] + }, + "LogEntry": { + "description": " A log entry message.\n\n A log message is similar to the `message` attribute on the event itself but\n can additionally hold optional parameters.\n\n ```json\n {\n \"message\": {\n \"message\": \"My raw message with interpreted strings like %s\",\n \"params\": [\"this\"]\n }\n }\n ```\n\n ```json\n {\n \"message\": {\n \"message\": \"My raw message with interpreted strings like {foo}\",\n \"params\": {\"foo\": \"this\"}\n }\n }\n ```", + "anyOf": [ + { + "type": "object", + "properties": { + "formatted": { + "description": " The formatted message. If `message` and `params` are given, Sentry\n will attempt to backfill `formatted` if empty.\n\n It must not exceed 8192 characters. Longer messages will be truncated.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Message" + }, + { + "type": "null" + } + ] + }, + "message": { + "description": " The log message with parameter placeholders.\n\n This attribute is primarily used for grouping related events together into issues.\n Therefore this really should just be a string template, i.e. `Sending %d requests` instead\n of `Sending 9999 requests`. The latter is much better at home in `formatted`.\n\n It must not exceed 8192 characters. Longer messages will be truncated.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Message" + }, + { + "type": "null" + } + ] + }, + "params": { + "description": " Parameters to be interpolated into the log message. This can be an array of positional\n parameters as well as a mapping of named arguments to their values.", + "default": null + } + }, + "additionalProperties": false + } + ] + }, + "MachException": { + "description": " Mach exception information.", + "anyOf": [ + { + "type": "object", + "properties": { + "code": { + "description": " The mach exception code.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "exception": { + "description": " The mach exception type.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "name": { + "description": " Optional name of the mach exception.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "subcode": { + "description": " The mach exception subcode.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "Mechanism": { + "description": " The mechanism by which an exception was generated and handled.\n\n The exception mechanism is an optional field residing in the [exception](#typedef-Exception).\n It carries additional information about the way the exception was created on the target system.\n This includes general exception values obtained from the operating system or runtime APIs, as\n well as mechanism-specific values.", + "anyOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "data": { + "description": " Arbitrary extra data that might help the user understand the error thrown by this mechanism.", + "default": null, + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "description": { + "description": " Optional human-readable description of the error mechanism.\n\n May include a possible hint on how to solve this error.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "handled": { + "description": " Flag indicating whether this exception was handled.\n\n This is a best-effort guess at whether the exception was handled by user code or not. For\n example:\n\n - Exceptions leading to a 500 Internal Server Error or to a hard process crash are\n `handled=false`, as the SDK typically has an integration that automatically captures the\n error.\n\n - Exceptions captured using `capture_exception` (called from user code) are `handled=true`\n as the user explicitly captured the exception (and therefore kind of handled it)", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "help_link": { + "description": " Link to online resources describing this error.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "meta": { + "description": " Operating system or runtime meta information.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/MechanismMeta" + }, + { + "type": "null" + } + ] + }, + "synthetic": { + "description": " If this is set then the exception is not a real exception but some\n form of synthetic error for instance from a signal handler, a hard\n segfault or similar where type and value are not useful for grouping\n or display purposes.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "type": { + "description": " Mechanism type (required).\n\n Required unique identifier of this mechanism determining rendering and processing of the\n mechanism data.\n\n In the Python SDK this is merely the name of the framework integration that produced the\n exception, while for native it is e.g. `\"minidump\"` or `\"applecrashreport\"`.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + ] + }, + "MechanismMeta": { + "description": " Operating system or runtime meta information to an exception mechanism.\n\n The mechanism metadata usually carries error codes reported by the runtime or operating system,\n along with a platform-dependent interpretation of these codes. SDKs can safely omit code names\n and descriptions for well-known error codes, as it will be filled out by Sentry. For\n proprietary or vendor-specific error codes, adding these values will give additional\n information to the user.", + "anyOf": [ + { + "type": "object", + "properties": { + "errno": { + "description": " Optional ISO C standard error code.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/CError" + }, + { + "type": "null" + } + ] + }, + "mach_exception": { + "description": " A Mach Exception on Apple systems comprising a code triple and optional descriptions.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/MachException" + }, + { + "type": "null" + } + ] + }, + "ns_error": { + "description": " An NSError on Apple systems comprising code and signal.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/NsError" + }, + { + "type": "null" + } + ] + }, + "signal": { + "description": " Information on the POSIX signal.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/PosixSignal" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + ] + }, + "Message": { + "anyOf": [ + { + "type": "string" + } + ] + }, + "MonitorContext": { + "description": " Monitor information.", + "anyOf": [ + { + "type": "object", + "additionalProperties": true + } + ] + }, + "NativeDebugImage": { + "description": " A generic (new-style) native platform debug information file.\n\n The `type` key must be one of:\n\n - `macho`\n - `elf`: ELF images are used on Linux platforms. Their structure is identical to other native images.\n - `pe`\n\n Examples:\n\n ```json\n {\n \"type\": \"elf\",\n \"code_id\": \"68220ae2c65d65c1b6aaa12fa6765a6ec2f5f434\",\n \"code_file\": \"/lib/x86_64-linux-gnu/libgcc_s.so.1\",\n \"debug_id\": \"e20a2268-5dc6-c165-b6aa-a12fa6765a6e\",\n \"image_addr\": \"0x7f5140527000\",\n \"image_size\": 90112,\n \"image_vmaddr\": \"0x40000\",\n \"arch\": \"x86_64\"\n }\n ```\n\n ```json\n {\n \"type\": \"pe\",\n \"code_id\": \"57898e12145000\",\n \"code_file\": \"C:\\\\Windows\\\\System32\\\\dbghelp.dll\",\n \"debug_id\": \"9c2a902b-6fdf-40ad-8308-588a41d572a0-1\",\n \"debug_file\": \"dbghelp.pdb\",\n \"image_addr\": \"0x70850000\",\n \"image_size\": \"1331200\",\n \"image_vmaddr\": \"0x40000\",\n \"arch\": \"x86\"\n }\n ```\n\n ```json\n {\n \"type\": \"macho\",\n \"debug_id\": \"84a04d24-0e60-3810-a8c0-90a65e2df61a\",\n \"debug_file\": \"libDiagnosticMessagesClient.dylib\",\n \"code_file\": \"/usr/lib/libDiagnosticMessagesClient.dylib\",\n \"image_addr\": \"0x7fffe668e000\",\n \"image_size\": 8192,\n \"image_vmaddr\": \"0x40000\",\n \"arch\": \"x86_64\",\n }\n ```", + "anyOf": [ + { + "type": "object", + "required": [ + "code_file", + "debug_id" + ], + "properties": { + "arch": { + "description": " CPU architecture target.\n\n Architecture of the module. If missing, this will be backfilled by Sentry.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "code_file": { + "description": " Path and name of the image file (required).\n\n The absolute path to the dynamic library or executable. This helps to locate the file if it is missing on Sentry.\n\n - `pe`: The code file should be provided to allow server-side stack walking of binary crash reports, such as Minidumps.", + "anyOf": [ + { + "$ref": "#/definitions/NativeImagePath" + }, + { + "type": "null" + } + ] + }, + "code_id": { + "description": " Optional identifier of the code file.\n\n - `elf`: If the program was compiled with a relatively recent compiler, this should be the hex representation of the `NT_GNU_BUILD_ID` program header (type `PT_NOTE`), or the value of the `.note.gnu.build-id` note section (type `SHT_NOTE`). Otherwise, leave this value empty.\n\n Certain symbol servers use the code identifier to locate debug information for ELF images, in which case this field should be included if possible.\n\n - `pe`: Identifier of the executable or DLL. It contains the values of the `time_date_stamp` from the COFF header and `size_of_image` from the optional header formatted together into a hex string using `%08x%X` (note that the second value is not padded):\n\n ```text\n time_date_stamp: 0x5ab38077\n size_of_image: 0x9000\n code_id: 5ab380779000\n ```\n\n The code identifier should be provided to allow server-side stack walking of binary crash reports, such as Minidumps.\n\n\n - `macho`: Identifier of the dynamic library or executable. It is the value of the `LC_UUID` load command in the Mach header, formatted as UUID. Can be empty for Mach images, as it is equivalent to the debug identifier.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/CodeId" + }, + { + "type": "null" + } + ] + }, + "debug_file": { + "description": " Path and name of the debug companion file.\n\n - `elf`: Name or absolute path to the file containing stripped debug information for this image. This value might be _required_ to retrieve debug files from certain symbol servers.\n\n - `pe`: Name of the PDB file containing debug information for this image. This value is often required to retrieve debug files from specific symbol servers.\n\n - `macho`: Name or absolute path to the dSYM file containing debug information for this image. This value might be required to retrieve debug files from certain symbol servers.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/NativeImagePath" + }, + { + "type": "null" + } + ] + }, + "debug_id": { + "description": " Unique debug identifier of the image.\n\n - `elf`: Debug identifier of the dynamic library or executable. If a code identifier is available, the debug identifier is the little-endian UUID representation of the first 16-bytes of that\n identifier. Spaces are inserted for readability, note the byte order of the first fields:\n\n ```text\n code id: f1c3bcc0 2798 65fe 3058 404b2831d9e6 4135386c\n debug id: c0bcc3f1-9827-fe65-3058-404b2831d9e6\n ```\n\n If no code id is available, the debug id should be computed by XORing the first 4096 bytes of the `.text` section in 16-byte chunks, and representing it as a little-endian UUID (again swapping the byte order).\n\n - `pe`: `signature` and `age` of the PDB file. Both values can be read from the CodeView PDB70 debug information header in the PE. The value should be represented as little-endian UUID, with the age appended at the end. Note that the byte order of the UUID fields must be swapped (spaces inserted for readability):\n\n ```text\n signature: f1c3bcc0 2798 65fe 3058 404b2831d9e6\n age: 1\n debug_id: c0bcc3f1-9827-fe65-3058-404b2831d9e6-1\n ```\n\n - `macho`: Identifier of the dynamic library or executable. It is the value of the `LC_UUID` load command in the Mach header, formatted as UUID.", + "anyOf": [ + { + "$ref": "#/definitions/DebugId" + }, + { + "type": "null" + } + ] + }, + "image_addr": { + "description": " Starting memory address of the image (required).\n\n Memory address, at which the image is mounted in the virtual address space of the process. Should be a string in hex representation prefixed with `\"0x\"`.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "image_size": { + "description": " Size of the image in bytes (required).\n\n The size of the image in virtual memory. If missing, Sentry will assume that the image spans up to the next image, which might lead to invalid stack traces.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "image_vmaddr": { + "description": " Loading address in virtual memory.\n\n Preferred load address of the image in virtual memory, as declared in the headers of the\n image. When loading an image, the operating system may still choose to place it at a\n different address.\n\n Symbols and addresses in the native image are always relative to the start of the image and do not consider the preferred load address. It is merely a hint to the loader.\n\n - `elf`/`macho`: If this value is non-zero, all symbols and addresses declared in the native image start at this address, rather than 0. By contrast, Sentry deals with addresses relative to the start of the image. For example, with `image_vmaddr: 0x40000`, a symbol located at `0x401000` has a relative address of `0x1000`.\n\n Relative addresses used in Apple Crash Reports and `addr2line` are usually in the preferred address space, and not relative address space.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + ] + }, + "NativeImagePath": { + "description": " A type for strings that are generally paths, might contain system user names, but still cannot\n be stripped liberally because it would break processing for certain platforms.\n\n Those strings get special treatment in our PII processor to avoid stripping the basename.", + "anyOf": [ + { + "type": "string" + } + ] + }, + "NsError": { + "description": " NSError informaiton.", + "anyOf": [ + { + "type": "object", + "properties": { + "code": { + "description": " The error code.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "domain": { + "description": " A string containing the error domain.", + "default": null, + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + ] + }, + "OsContext": { + "description": " Operating system information.\n\n OS context describes the operating system on which the event was created. In web contexts, this\n is the operating system of the browser (generally pulled from the User-Agent string).", + "anyOf": [ + { + "type": "object", + "properties": { + "build": { + "description": " Internal build number of the operating system.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "kernel_version": { + "description": " Current kernel version.\n\n This is typically the entire output of the `uname` syscall.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "name": { + "description": " Name of the operating system.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "raw_description": { + "description": " Unprocessed operating system info.\n\n An unprocessed description string obtained by the operating system. For some well-known\n runtimes, Sentry will attempt to parse `name` and `version` from this string, if they are\n not explicitly given.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "rooted": { + "description": " Indicator if the OS is rooted (mobile mostly).", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "version": { + "description": " Version of the operating system.", + "default": null, + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + ] + }, + "PosixSignal": { + "description": " POSIX signal with optional extended data.\n\n On Apple systems, signals also carry a code in addition to the signal number describing the\n signal in more detail. On Linux, this code does not exist.", + "anyOf": [ + { + "type": "object", + "properties": { + "code": { + "description": " An optional signal code present on Apple systems.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "code_name": { + "description": " Optional name of the errno constant.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "name": { + "description": " Optional name of the errno constant.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "number": { + "description": " The POSIX signal number.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "int64" + } + }, + "additionalProperties": false + } + ] + }, + "ProguardDebugImage": { + "description": " Proguard mapping file.\n\n Proguard images refer to `mapping.txt` files generated when Proguard obfuscates function names. The Java SDK integrations assign this file a unique identifier, which has to be included in the list of images.", + "anyOf": [ + { + "type": "object", + "required": [ + "uuid" + ], + "properties": { + "uuid": { + "description": " UUID computed from the file contents, assigned by the Java SDK.", + "type": [ + "string", + "null" + ], + "format": "uuid" + } + }, + "additionalProperties": false + } + ] + }, + "RawStacktrace": { + "description": " A stack trace of a single thread.\n\n A stack trace contains a list of frames, each with various bits (most optional) describing the context of that frame. Frames should be sorted from oldest to newest.\n\n For the given example program written in Python:\n\n ```python\n def foo():\n my_var = 'foo'\n raise ValueError()\n\n def main():\n foo()\n ```\n\n A minimalistic stack trace for the above program in the correct order:\n\n ```json\n {\n \"frames\": [\n {\"function\": \"main\"},\n {\"function\": \"foo\"}\n ]\n }\n ```\n\n The top frame fully symbolicated with five lines of source context:\n\n ```json\n {\n \"frames\": [{\n \"in_app\": true,\n \"function\": \"myfunction\",\n \"abs_path\": \"/real/file/name.py\",\n \"filename\": \"file/name.py\",\n \"lineno\": 3,\n \"vars\": {\n \"my_var\": \"'value'\"\n },\n \"pre_context\": [\n \"def foo():\",\n \" my_var = 'foo'\",\n ],\n \"context_line\": \" raise ValueError()\",\n \"post_context\": [\n \"\",\n \"def main():\"\n ],\n }]\n }\n ```\n\n A minimal native stack trace with register values. Note that the `package` event attribute must be \"native\" for these frames to be symbolicated.\n\n ```json\n {\n \"frames\": [\n {\"instruction_addr\": \"0x7fff5bf3456c\"},\n {\"instruction_addr\": \"0x7fff5bf346c0\"},\n ],\n \"registers\": {\n \"rip\": \"0x00007ff6eef54be2\",\n \"rsp\": \"0x0000003b710cd9e0\"\n }\n }\n ```", + "anyOf": [ + { + "type": "object", + "required": [ + "frames" + ], + "properties": { + "frames": { + "description": " Required. A non-empty list of stack frames. The list is ordered from caller to callee, or oldest to youngest. The last frame is the one creating the exception.", + "type": [ + "array", + "null" + ], + "items": { + "anyOf": [ + { + "$ref": "#/definitions/Frame" + }, + { + "type": "null" + } + ] + } + }, + "lang": { + "description": " The language of the stacktrace.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "registers": { + "description": " Register values of the thread (top frame).\n\n A map of register names and their values. The values should contain the actual register values of the thread, thus mapping to the last frame in the list.", + "default": null, + "type": [ + "object", + "null" + ], + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/definitions/RegVal" + }, + { + "type": "null" + } + ] + } + }, + "snapshot": { + "description": " Indicates that this stack trace is a snapshot triggered by an external signal.\n\n If this field is `false`, then the stack trace points to the code that caused this stack\n trace to be created. This can be the location of a raised exception, as well as an exception\n or signal handler.\n\n If this field is `true`, then the stack trace was captured as part of creating an unrelated\n event. For example, a thread other than the crashing thread, or a stack trace computed as a\n result of an external kill signal.", + "default": null, + "type": [ + "boolean", + "null" + ] + } + }, + "additionalProperties": false + } + ] + }, + "RegVal": { + "type": "string" + }, + "Request": { + "description": " Http request information.\n\n The Request interface contains information on a HTTP request related to the event. In client\n SDKs, this can be an outgoing request, or the request that rendered the current web page. On\n server SDKs, this could be the incoming web request that is being handled.\n\n The data variable should only contain the request body (not the query string). It can either be\n a dictionary (for standard HTTP requests) or a raw request body.\n\n ### Ordered Maps\n\n In the Request interface, several attributes can either be declared as string, object, or list\n of tuples. Sentry attempts to parse structured information from the string representation in\n such cases.\n\n Sometimes, keys can be declared multiple times, or the order of elements matters. In such\n cases, use the tuple representation over a plain object.\n\n Example of request headers as object:\n\n ```json\n {\n \"content-type\": \"application/json\",\n \"accept\": \"application/json, application/xml\"\n }\n ```\n\n Example of the same headers as list of tuples:\n\n ```json\n [\n [\"content-type\", \"application/json\"],\n [\"accept\", \"application/json\"],\n [\"accept\", \"application/xml\"]\n ]\n ```\n\n Example of a fully populated request object:\n\n ```json\n {\n \"request\": {\n \"method\": \"POST\",\n \"url\": \"http://absolute.uri/foo\",\n \"query_string\": \"query=foobar&page=2\",\n \"data\": {\n \"foo\": \"bar\"\n },\n \"cookies\": \"PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;\",\n \"headers\": {\n \"content-type\": \"text/html\"\n },\n \"env\": {\n \"REMOTE_ADDR\": \"192.168.0.1\"\n }\n }\n }\n ```", + "anyOf": [ + { + "type": "object", + "properties": { + "cookies": { + "description": " The cookie values.\n\n Can be given unparsed as string, as dictionary, or as a list of tuples.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Cookies" + }, + { + "type": "null" + } + ] + }, + "data": { + "description": " Request data in any format that makes sense.\n\n SDKs should discard large and binary bodies by default. Can be given as string or\n structural data of any format.", + "default": null + }, + "env": { + "description": " Server environment data, such as CGI/WSGI.\n\n A dictionary containing environment information passed from the server. This is where\n information such as CGI/WSGI/Rack keys go that are not HTTP headers.\n\n Sentry will explicitly look for `REMOTE_ADDR` to extract an IP address.", + "default": null, + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "fragment": { + "description": " The fragment of the request URL.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "headers": { + "description": " A dictionary of submitted headers.\n\n If a header appears multiple times it, needs to be merged according to the HTTP standard\n for header merging. Header names are treated case-insensitively by Sentry.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Headers" + }, + { + "type": "null" + } + ] + }, + "inferred_content_type": { + "description": " The inferred content type of the request payload.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "method": { + "description": " HTTP request method.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "query_string": { + "description": " The query string component of the URL.\n\n Can be given as unparsed string, dictionary, or list of tuples.\n\n If the query string is not declared and part of the `url`, Sentry moves it to the\n query string.", + "default": null, + "anyOf": [ + { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": [ + "string", + "null" + ] + } + }, + { + "type": "array", + "items": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "string", + "null" + ] + } + ], + "maxItems": 2, + "minItems": 2 + } + } + ] + } + ] + }, + { + "type": "null" + } + ] + }, + "url": { + "description": " The URL of the request if available.\n\nThe query string can be declared either as part of the `url`, or separately in `query_string`.", + "default": null, + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + ] + }, + "RuntimeContext": { + "description": " Runtime information.\n\n Runtime context describes a runtime in more detail. Typically, this context is present in\n `contexts` multiple times if multiple runtimes are involved (for instance, if you have a\n JavaScript application running on top of JVM).", + "anyOf": [ + { + "type": "object", + "properties": { + "build": { + "description": " Application build string, if it is separate from the version.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "name": { + "description": " Runtime name.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "raw_description": { + "description": " Unprocessed runtime info.\n\n An unprocessed description string obtained by the runtime. For some well-known runtimes,\n Sentry will attempt to parse `name` and `version` from this string, if they are not\n explicitly given.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "version": { + "description": " Runtime version string.", + "default": null, + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + ] + }, + "SpanId": { + "description": " A 16-character hex string as described in the W3C trace context spec.", + "anyOf": [ + { + "type": "string" + } + ] + }, + "SpanStatus": { + "description": "Trace status.\n\nValues from Mapping to HTTP from ", + "type": "string", + "enum": [ + "ok", + "cancelled", + "unknown", + "invalid_argument", + "deadline_exceeded", + "not_found", + "already_exists", + "permission_denied", + "resource_exhausted", + "failed_precondition", + "aborted", + "out_of_range", + "unimplemented", + "internal_error", + "unavailable", + "data_loss", + "unauthenticated" + ] + }, + "Stacktrace": { + "anyOf": [ + { + "$ref": "#/definitions/RawStacktrace" + } + ] + }, + "String": { + "type": "string" + }, + "SystemSdkInfo": { + "description": " Holds information about the system SDK.\n\n This is relevant for iOS and other platforms that have a system\n SDK. Not to be confused with the client SDK.", + "anyOf": [ + { + "type": "object", + "properties": { + "sdk_name": { + "description": " The internal name of the SDK.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "version_major": { + "description": " The major version of the SDK as integer or 0.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "version_minor": { + "description": " The minor version of the SDK as integer or 0.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "version_patchlevel": { + "description": " The patch version of the SDK as integer or 0.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "TagEntry": { + "anyOf": [ + { + "type": "array", + "items": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "string", + "null" + ] + } + ], + "maxItems": 2, + "minItems": 2 + } + ] + }, + "Tags": { + "description": " Manual key/value tag pairs.", + "anyOf": [ + { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": [ + "string", + "null" + ] + } + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/TagEntry" + }, + { + "type": "null" + } + ] + } + } + ] + } + ] + }, + "Thread": { + "description": " A process thread of an event.\n\n The Threads Interface specifies threads that were running at the time an event happened. These threads can also contain stack traces.\n\n An event may contain one or more threads in an attribute named `threads`.\n\n The following example illustrates the threads part of the event payload and omits other attributes for simplicity.\n\n ```json\n {\n \"threads\": {\n \"values\": [\n {\n \"id\": \"0\",\n \"name\": \"main\",\n \"crashed\": true,\n \"stacktrace\": {}\n }\n ]\n }\n }\n ```", + "anyOf": [ + { + "type": "object", + "properties": { + "crashed": { + "description": " A flag indicating whether the thread crashed. Defaults to `false`.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "current": { + "description": " A flag indicating whether the thread was in the foreground. Defaults to `false`.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "id": { + "description": " The ID of the thread. Typically a number or numeric string.\n\n Needs to be unique among the threads. An exception can set the `thread_id` attribute to cross-reference this thread.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/ThreadId" + }, + { + "type": "null" + } + ] + }, + "name": { + "description": " Display name of this thread.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "stacktrace": { + "description": " Stack trace containing frames of this exception.\n\n The thread that crashed with an exception should not have a stack trace, but instead, the `thread_id` attribute should be set on the exception and Sentry will connect the two.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Stacktrace" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + ] + }, + "ThreadId": { + "description": " Represents a thread id.", + "anyOf": [ + { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + { + "type": "string" + } + ] + }, + "Timestamp": { + "description": "Can be a ISO-8601 formatted string or a unix timestamp in seconds (floating point values allowed).\n\nMust be UTC.", + "anyOf": [ + { + "type": "number", + "format": "double" + }, + { + "type": "string" + } + ] + }, + "TraceContext": { + "description": " Trace context", + "anyOf": [ + { + "type": "object", + "required": [ + "span_id", + "trace_id" + ], + "properties": { + "exclusive_time": { + "description": " The amount of time in milliseconds spent in this transaction span,\n excluding its immediate child spans.", + "default": null, + "type": [ + "number", + "null" + ], + "format": "double" + }, + "op": { + "description": " Span type (see `OperationType` docs).", + "default": null, + "type": [ + "string", + "null" + ] + }, + "parent_span_id": { + "description": " The ID of the span enclosing this span.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/SpanId" + }, + { + "type": "null" + } + ] + }, + "span_id": { + "description": " The ID of the span.", + "anyOf": [ + { + "$ref": "#/definitions/SpanId" + }, + { + "type": "null" + } + ] + }, + "status": { + "description": " Whether the trace failed or succeeded. Currently only used to indicate status of individual\n transactions.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/SpanStatus" + }, + { + "type": "null" + } + ] + }, + "trace_id": { + "description": " The trace ID.", + "anyOf": [ + { + "$ref": "#/definitions/TraceId" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + ] + }, + "TraceId": { + "description": " A 32-character hex string as described in the W3C trace context spec.", + "anyOf": [ + { + "type": "string" + } + ] + }, + "User": { + "description": " Information about the user who triggered an event.\n\n ```json\n {\n \"user\": {\n \"id\": \"unique_id\",\n \"username\": \"my_user\",\n \"email\": \"foo@example.com\",\n \"ip_address\": \"127.0.0.1\",\n \"subscription\": \"basic\"\n }\n }\n ```", + "anyOf": [ + { + "type": "object", + "properties": { + "data": { + "description": " Additional arbitrary fields, as stored in the database (and sometimes as sent by clients).\n All data from `self.other` should end up here after store normalization.", + "default": null, + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "email": { + "description": " Email address of the user.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "geo": { + "description": " Approximate geographical location of the end user or device.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Geo" + }, + { + "type": "null" + } + ] + }, + "id": { + "description": " Unique identifier of the user.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "ip_address": { + "description": " Remote IP address of the user. Defaults to \"{{auto}}\".", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/String" + }, + { + "type": "null" + } + ] + }, + "name": { + "description": " Human readable name of the user.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "segment": { + "description": " The user segment, for apps that divide users in user segments.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "username": { + "description": " Username of the user.", + "default": null, + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/relay-server/src/actors/envelopes.rs b/relay-server/src/actors/envelopes.rs index cef7a943782..f1792890bd4 100644 --- a/relay-server/src/actors/envelopes.rs +++ b/relay-server/src/actors/envelopes.rs @@ -1345,7 +1345,6 @@ impl EnvelopeProcessor { // `process_event`. let event_item = envelope.take_item_by(|item| item.ty() == &ItemType::Event); let transaction_item = envelope.take_item_by(|item| item.ty() == &ItemType::Transaction); - let replay_item = envelope.take_item_by(|item| item.ty() == &ItemType::ReplayEvent); 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 form_item = envelope.take_item_by(|item| item.ty() == &ItemType::FormData); @@ -1377,12 +1376,6 @@ impl EnvelopeProcessor { // hint to normalization that we're dealing with a transaction now. self.event_from_json_payload(item, Some(EventType::Transaction))? }) - } else if let Some(mut item) = replay_item { - relay_log::trace!("processing json replay event"); - state.sample_rates = item.take_sample_rates(); - metric!(timer(RelayTimers::EventProcessingDeserialize), { - self.event_from_json_payload(item, Some(EventType::ReplayEvent))? - }) } else if let Some(mut item) = raw_security_item { relay_log::trace!("processing security report"); state.sample_rates = item.take_sample_rates(); diff --git a/relay-server/src/envelope.rs b/relay-server/src/envelope.rs index a2a8f35329b..0954a201d45 100644 --- a/relay-server/src/envelope.rs +++ b/relay-server/src/envelope.rs @@ -122,7 +122,6 @@ impl ItemType { match event_type { EventType::Default | EventType::Error => ItemType::Event, EventType::Transaction => ItemType::Transaction, - EventType::ReplayEvent => ItemType::ReplayEvent, EventType::Csp | EventType::Hpkp | EventType::ExpectCt | EventType::ExpectStaple => { ItemType::Security } @@ -604,7 +603,6 @@ impl Item { | ItemType::Transaction | ItemType::Security | ItemType::RawSecurity - | ItemType::ReplayEvent | ItemType::UnrealReport => true, // Attachments are only event items if they are crash reports or if they carry partial @@ -632,6 +630,7 @@ impl Item { | ItemType::MetricBuckets | ItemType::ClientReport | ItemType::ReplayPayload + | ItemType::ReplayEvent | ItemType::Profile => false, // The unknown item type can observe any behavior, most likely there are going to be no diff --git a/relay-server/src/metrics_extraction/transactions.rs b/relay-server/src/metrics_extraction/transactions.rs index 95a66f95ce7..8db8f0b0117 100644 --- a/relay-server/src/metrics_extraction/transactions.rs +++ b/relay-server/src/metrics_extraction/transactions.rs @@ -290,9 +290,7 @@ fn extract_transaction_metrics_inner( event: &Event, mut push_metric: impl FnMut(Metric), ) { - if event.ty.value() != Some(&EventType::Transaction) - && event.ty.value() != Some(&EventType::ReplayEvent) - { + if event.ty.value() != Some(&EventType::Transaction) { return; } diff --git a/tests/integration/test_replay_event.py b/tests/integration/test_replay_event.py index d32838e4d50..204f2206303 100644 --- a/tests/integration/test_replay_event.py +++ b/tests/integration/test_replay_event.py @@ -68,14 +68,7 @@ def test_replay_event(mini_sentry, relay_with_processing, replay_events_consumer event, _ = replay_events_consumer.get_replay_event() assert event["transaction"] == "/organizations/:orgId/performance/:eventSlug/" assert "trace" in event["contexts"] - assert "measurements" in event, event - assert "spans" in event, event - assert event["measurements"] == { - "lcp": {"value": 420.69}, - "lcp_final.element-size123": {"value": 1}, - "fid": {"value": 2020}, - "cls": {"value": None}, - "fp": {"value": None}, - "missing_value": None, - } + assert "measurements" in event + assert "spans" in event + assert "measurements" in event assert "breadcrumbs" in event From db13fefede438503d31f3e64a318ca0662e3a50a Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Tue, 24 May 2022 20:50:03 -0700 Subject: [PATCH 13/24] update schema snapshot --- .../test_fixtures__event_schema.snap | 1 - .../test_fixtures__event_schema.snap.new | 2891 ----------------- 2 files changed, 2892 deletions(-) delete mode 100644 relay-general/tests/snapshots/test_fixtures__event_schema.snap.new diff --git a/relay-general/tests/snapshots/test_fixtures__event_schema.snap b/relay-general/tests/snapshots/test_fixtures__event_schema.snap index ed814968ea7..122693084e4 100644 --- a/relay-general/tests/snapshots/test_fixtures__event_schema.snap +++ b/relay-general/tests/snapshots/test_fixtures__event_schema.snap @@ -1228,7 +1228,6 @@ expression: event_json_schema() "expectct", "expectstaple", "transaction", - "replayevent", "default" ] }, diff --git a/relay-general/tests/snapshots/test_fixtures__event_schema.snap.new b/relay-general/tests/snapshots/test_fixtures__event_schema.snap.new deleted file mode 100644 index 122693084e4..00000000000 --- a/relay-general/tests/snapshots/test_fixtures__event_schema.snap.new +++ /dev/null @@ -1,2891 +0,0 @@ ---- -source: relay-general/tests/test_fixtures.rs -expression: event_json_schema() ---- -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Event", - "description": " The sentry v7 event structure.", - "anyOf": [ - { - "type": "object", - "properties": { - "breadcrumbs": { - "description": " List of breadcrumbs recorded before this event.", - "default": null, - "type": [ - "object", - "null" - ], - "required": [ - "values" - ], - "properties": { - "values": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/definitions/Breadcrumb" - }, - { - "type": "null" - } - ] - } - } - } - }, - "contexts": { - "description": " Contexts describing the environment (e.g. device, os or browser).", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Contexts" - }, - { - "type": "null" - } - ] - }, - "culprit": { - "description": " Custom culprit of the event.\n\n This field is deprecated and shall not be set by client SDKs.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "debug_meta": { - "description": " Meta data for event processing and debugging.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/DebugMeta" - }, - { - "type": "null" - } - ] - }, - "dist": { - "description": " Program's distribution identifier.\n\n The distribution of the application.\n\n Distributions are used to disambiguate build or deployment variants of the same release of\n an application. For example, the dist can be the build number of an XCode build or the\n version code of an Android build.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "environment": { - "description": " The environment name, such as `production` or `staging`.\n\n ```json\n { \"environment\": \"production\" }\n ```", - "default": null, - "type": [ - "string", - "null" - ] - }, - "errors": { - "description": " Errors encountered during processing. Intended to be phased out in favor of\n annotation/metadata system.", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "anyOf": [ - { - "$ref": "#/definitions/EventProcessingError" - }, - { - "type": "null" - } - ] - } - }, - "event_id": { - "description": " Unique identifier of this event.\n\n Hexadecimal string representing a uuid4 value. The length is exactly 32 characters. Dashes\n are not allowed. Has to be lowercase.\n\n Even though this field is backfilled on the server with a new uuid4, it is strongly\n recommended to generate that uuid4 clientside. There are some features like user feedback\n which are easier to implement that way, and debugging in case events get lost in your\n Sentry installation is also easier.\n\n Example:\n\n ```json\n {\n \"event_id\": \"fc6d8c0c43fc4630ad850ee518f1b9d0\"\n }\n ```", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/EventId" - }, - { - "type": "null" - } - ] - }, - "exception": { - "description": " One or multiple chained (nested) exceptions.", - "default": null, - "type": [ - "object", - "null" - ], - "required": [ - "values" - ], - "properties": { - "values": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/definitions/Exception" - }, - { - "type": "null" - } - ] - } - } - } - }, - "extra": { - "description": " Arbitrary extra information set by the user.\n\n ```json\n {\n \"extra\": {\n \"my_key\": 1,\n \"some_other_value\": \"foo bar\"\n }\n }```", - "default": null, - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "fingerprint": { - "description": " Manual fingerprint override.\n\n A list of strings used to dictate how this event is supposed to be grouped with other\n events into issues. For more information about overriding grouping see [Customize Grouping\n with Fingerprints](https://docs.sentry.io/data-management/event-grouping/).\n\n ```json\n {\n \"fingerprint\": [\"myrpc\", \"POST\", \"/foo.bar\"]\n }", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Fingerprint" - }, - { - "type": "null" - } - ] - }, - "level": { - "description": " Severity level of the event. Defaults to `error`.\n\n Example:\n\n ```json\n {\"level\": \"warning\"}\n ```", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Level" - }, - { - "type": "null" - } - ] - }, - "logentry": { - "description": " Custom parameterized message for this event.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/LogEntry" - }, - { - "type": "null" - } - ] - }, - "logger": { - "description": " Logger that created the event.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "modules": { - "description": " Name and versions of all installed modules/packages/dependencies in the current\n environment/application.\n\n ```json\n { \"django\": \"3.0.0\", \"celery\": \"4.2.1\" }\n ```\n\n In Python this is a list of installed packages as reported by `pkg_resources` together with\n their reported version string.\n\n This is primarily used for suggesting to enable certain SDK integrations from within the UI\n and for making informed decisions on which frameworks to support in future development\n efforts.", - "default": null, - "type": [ - "object", - "null" - ], - "additionalProperties": { - "type": [ - "string", - "null" - ] - } - }, - "platform": { - "description": " Platform identifier of this event (defaults to \"other\").\n\n A string representing the platform the SDK is submitting from. This will be used by the\n Sentry interface to customize various components in the interface, but also to enter or\n skip stacktrace processing.\n\n Acceptable values are: `as3`, `c`, `cfml`, `cocoa`, `csharp`, `elixir`, `haskell`, `go`,\n `groovy`, `java`, `javascript`, `native`, `node`, `objc`, `other`, `perl`, `php`, `python`,\n `ruby`", - "default": null, - "type": [ - "string", - "null" - ] - }, - "received": { - "description": " Timestamp when the event has been received by Sentry.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Timestamp" - }, - { - "type": "null" - } - ] - }, - "release": { - "description": " The release version of the application.\n\n **Release versions must be unique across all projects in your organization.** This value\n can be the git SHA for the given project, or a product identifier with a semantic version.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "request": { - "description": " Information about a web request that occurred during the event.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Request" - }, - { - "type": "null" - } - ] - }, - "sdk": { - "description": " Information about the Sentry SDK that generated this event.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/ClientSdkInfo" - }, - { - "type": "null" - } - ] - }, - "server_name": { - "description": " Server or device name the event was generated on.\n\n This is supposed to be a hostname.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "stacktrace": { - "description": " Event stacktrace.\n\n DEPRECATED: Prefer `threads` or `exception` depending on which is more appropriate.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Stacktrace" - }, - { - "type": "null" - } - ] - }, - "tags": { - "description": " Custom tags for this event.\n\n A map or list of tags for this event. Each tag must be less than 200 characters.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Tags" - }, - { - "type": "null" - } - ] - }, - "threads": { - "description": " Threads that were active when the event occurred.", - "default": null, - "type": [ - "object", - "null" - ], - "required": [ - "values" - ], - "properties": { - "values": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/definitions/Thread" - }, - { - "type": "null" - } - ] - } - } - } - }, - "time_spent": { - "description": " Time since the start of the transaction until the error occurred.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "timestamp": { - "description": " Timestamp when the event was created.\n\n Indicates when the event was created in the Sentry SDK. The format is either a string as\n defined in [RFC 3339](https://tools.ietf.org/html/rfc3339) or a numeric (integer or float)\n value representing the number of seconds that have elapsed since the [Unix\n epoch](https://en.wikipedia.org/wiki/Unix_time).\n\n Timezone is assumed to be UTC if missing.\n\n Sub-microsecond precision is not preserved with numeric values due to precision\n limitations with floats (at least in our systems). With that caveat in mind, just send\n whatever is easiest to produce.\n\n All timestamps in the event protocol are formatted this way.\n\n # Example\n\n All of these are the same date:\n\n ```json\n { \"timestamp\": \"2011-05-02T17:41:36Z\" }\n { \"timestamp\": \"2011-05-02T17:41:36\" }\n { \"timestamp\": \"2011-05-02T17:41:36.000\" }\n { \"timestamp\": 1304358096.0 }\n ```", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Timestamp" - }, - { - "type": "null" - } - ] - }, - "transaction": { - "description": " Transaction name of the event.\n\n For example, in a web app, this might be the route name (`\"/users//\"` or\n `UserView`), in a task queue it might be the function + module name.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "type": { - "description": " Type of the event. Defaults to `default`.\n\n The event type determines how Sentry handles the event and has an impact on processing, rate\n limiting, and quotas. There are three fundamental classes of event types:\n\n - **Error monitoring events**: Processed and grouped into unique issues based on their\n exception stack traces and error messages.\n - **Security events**: Derived from Browser security violation reports and grouped into\n unique issues based on the endpoint and violation. SDKs do not send such events.\n - **Transaction events** (`transaction`): Contain operation spans and collected into traces\n for performance monitoring.\n\n Transactions must explicitly specify the `\"transaction\"` event type. In all other cases,\n Sentry infers the appropriate event type from the payload and overrides the stated type.\n SDKs should not send an event type other than for transactions.\n\n Example:\n\n ```json\n {\n \"type\": \"transaction\",\n \"spans\": []\n }\n ```", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/EventType" - }, - { - "type": "null" - } - ] - }, - "user": { - "description": " Information about the user who triggered this event.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/User" - }, - { - "type": "null" - } - ] - }, - "version": { - "description": " Version", - "default": null, - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "type": "string" - }, - "AppContext": { - "description": " Application information.\n\n App context describes the application. As opposed to the runtime, this is the actual\n application that was running and carries metadata about the current session.", - "anyOf": [ - { - "type": "object", - "properties": { - "app_build": { - "description": " Internal build ID as it appears on the platform.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "app_identifier": { - "description": " Version-independent application identifier, often a dotted bundle ID.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "app_name": { - "description": " Application name as it appears on the platform.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "app_start_time": { - "description": " Start time of the app.\n\n Formatted UTC timestamp when the user started the application.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "app_version": { - "description": " Application version as it appears on the platform.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "build_type": { - "description": " String identifying the kind of build. For example, `testflight`.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "device_app_hash": { - "description": " Application-specific device identifier.", - "default": null, - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - ] - }, - "AppleDebugImage": { - "description": " Legacy apple debug images (MachO).\n\n This was also used for non-apple platforms with similar debug setups.", - "anyOf": [ - { - "type": "object", - "required": [ - "image_addr", - "image_size", - "name", - "uuid" - ], - "properties": { - "arch": { - "description": " CPU architecture target.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "cpu_subtype": { - "description": " MachO CPU subtype identifier.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "cpu_type": { - "description": " MachO CPU type identifier.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "image_addr": { - "description": " Starting memory address of the image (required).", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - }, - "image_size": { - "description": " Size of the image in bytes (required).", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "image_vmaddr": { - "description": " Loading address in virtual memory.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - }, - "name": { - "description": " Path and name of the debug image (required).", - "type": [ - "string", - "null" - ] - }, - "uuid": { - "description": " The unique UUID of the image.", - "type": [ - "string", - "null" - ], - "format": "uuid" - } - }, - "additionalProperties": false - } - ] - }, - "Breadcrumb": { - "description": " The Breadcrumbs Interface specifies a series of application events, or \"breadcrumbs\", that\n occurred before an event.\n\n An event may contain one or more breadcrumbs in an attribute named `breadcrumbs`. The entries\n are ordered from oldest to newest. Consequently, the last entry in the list should be the last\n entry before the event occurred.\n\n While breadcrumb attributes are not strictly validated in Sentry, a breadcrumb is most useful\n when it includes at least a `timestamp` and `type`, `category` or `message`. The rendering of\n breadcrumbs in Sentry depends on what is provided.\n\n The following example illustrates the breadcrumbs part of the event payload and omits other\n attributes for simplicity.\n\n ```json\n {\n \"breadcrumbs\": {\n \"values\": [\n {\n \"timestamp\": \"2016-04-20T20:55:53.845Z\",\n \"message\": \"Something happened\",\n \"category\": \"log\",\n \"data\": {\n \"foo\": \"bar\",\n \"blub\": \"blah\"\n }\n },\n {\n \"timestamp\": \"2016-04-20T20:55:53.847Z\",\n \"type\": \"navigation\",\n \"data\": {\n \"from\": \"/login\",\n \"to\": \"/dashboard\"\n }\n }\n ]\n }\n }\n ```", - "anyOf": [ - { - "type": "object", - "properties": { - "category": { - "description": " A dotted string indicating what the crumb is or from where it comes. _Optional._\n\n Typically it is a module name or a descriptive string. For instance, _ui.click_ could be\n used to indicate that a click happened in the UI or _flask_ could be used to indicate that\n the event originated in the Flask framework.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "data": { - "description": " Arbitrary data associated with this breadcrumb.\n\n Contains a dictionary whose contents depend on the breadcrumb `type`. Additional parameters\n that are unsupported by the type are rendered as a key/value table.", - "default": null, - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "event_id": { - "description": " Identifier of the event this breadcrumb belongs to.\n\n Sentry events can appear as breadcrumbs in other events as long as they have occurred in the\n same organization. This identifier links to the original event.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/EventId" - }, - { - "type": "null" - } - ] - }, - "level": { - "description": " Severity level of the breadcrumb. _Optional._\n\n Allowed values are, from highest to lowest: `fatal`, `error`, `warning`, `info`, and\n `debug`. Levels are used in the UI to emphasize and deemphasize the crumb. Defaults to\n `info`.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Level" - }, - { - "type": "null" - } - ] - }, - "message": { - "description": " Human readable message for the breadcrumb.\n\n If a message is provided, it is rendered as text with all whitespace preserved. Very long\n text might be truncated in the UI.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "timestamp": { - "description": " The timestamp of the breadcrumb. Recommended.\n\n A timestamp representing when the breadcrumb occurred. The format is either a string as\n defined in [RFC 3339](https://tools.ietf.org/html/rfc3339) or a numeric (integer or float)\n value representing the number of seconds that have elapsed since the [Unix\n epoch](https://en.wikipedia.org/wiki/Unix_time).\n\n Breadcrumbs are most useful when they include a timestamp, as it creates a timeline leading\n up to an event.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Timestamp" - }, - { - "type": "null" - } - ] - }, - "type": { - "description": " The type of the breadcrumb. _Optional_, defaults to `default`.\n\n - `default`: Describes a generic breadcrumb. This is typically a log message or\n user-generated breadcrumb. The `data` field is entirely undefined and as such, completely\n rendered as a key/value table.\n\n - `navigation`: Describes a navigation breadcrumb. A navigation event can be a URL change\n in a web application, or a UI transition in a mobile or desktop application, etc.\n\n Such a breadcrumb's `data` object has the required fields `from` and `to`, which\n represent an application route/url each.\n\n - `http`: Describes an HTTP request breadcrumb. This represents an HTTP request transmitted\n from your application. This could be an AJAX request from a web application, or a\n server-to-server HTTP request to an API service provider, etc.\n\n Such a breadcrumb's `data` property has the fields `url`, `method`, `status_code`\n (integer) and `reason` (string).", - "default": null, - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - ] - }, - "BrowserContext": { - "description": " Web browser information.", - "anyOf": [ - { - "type": "object", - "properties": { - "name": { - "description": " Display name of the browser application.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "version": { - "description": " Version string of the browser.", - "default": null, - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - ] - }, - "CError": { - "description": " POSIX signal with optional extended data.\n\n Error codes set by Linux system calls and some library functions as specified in ISO C99,\n POSIX.1-2001, and POSIX.1-2008. See\n [`errno(3)`](https://man7.org/linux/man-pages/man3/errno.3.html) for more information.", - "anyOf": [ - { - "type": "object", - "properties": { - "name": { - "description": " Optional name of the errno constant.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "number": { - "description": " The error code as specified by ISO C99, POSIX.1-2001 or POSIX.1-2008.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "int64" - } - }, - "additionalProperties": false - } - ] - }, - "ClientSdkInfo": { - "description": " The SDK Interface describes the Sentry SDK and its configuration used to capture and transmit an event.", - "anyOf": [ - { - "type": "object", - "required": [ - "name", - "version" - ], - "properties": { - "integrations": { - "description": " List of integrations that are enabled in the SDK. _Optional._\n\n The list should have all enabled integrations, including default integrations. Default\n integrations are included because different SDK releases may contain different default\n integrations.", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "type": [ - "string", - "null" - ] - } - }, - "name": { - "description": " Unique SDK name. _Required._\n\n The name of the SDK. The format is `entity.ecosystem[.flavor]` where entity identifies the\n developer of the SDK, ecosystem refers to the programming language or platform where the\n SDK is to be used and the optional flavor is used to identify standalone SDKs that are part\n of a major ecosystem.\n\n Official Sentry SDKs use the entity `sentry`, as in `sentry.python` or\n `sentry.javascript.react-native`. Please use a different entity for your own SDKs.", - "type": [ - "string", - "null" - ] - }, - "packages": { - "description": " List of installed and loaded SDK packages. _Optional._\n\n A list of packages that were installed as part of this SDK or the activated integrations.\n Each package consists of a name in the format `source:identifier` and `version`. If the\n source is a Git repository, the `source` should be `git`, the identifier should be a\n checkout link and the version should be a Git reference (branch, tag or SHA).", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "anyOf": [ - { - "$ref": "#/definitions/ClientSdkPackage" - }, - { - "type": "null" - } - ] - } - }, - "version": { - "description": " The version of the SDK. _Required._\n\n It should have the [Semantic Versioning](https://semver.org/) format `MAJOR.MINOR.PATCH`,\n without any prefix (no `v` or anything else in front of the major version number).\n\n Examples: `0.1.0`, `1.0.0`, `4.3.12`", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - ] - }, - "ClientSdkPackage": { - "description": " An installed and loaded package as part of the Sentry SDK.", - "anyOf": [ - { - "type": "object", - "properties": { - "name": { - "description": " Name of the package.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "version": { - "description": " Version of the package.", - "default": null, - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - ] - }, - "CodeId": { - "type": "string" - }, - "Context": { - "description": " A context describes environment info (e.g. device, os or browser).", - "anyOf": [ - { - "$ref": "#/definitions/DeviceContext" - }, - { - "$ref": "#/definitions/OsContext" - }, - { - "$ref": "#/definitions/RuntimeContext" - }, - { - "$ref": "#/definitions/AppContext" - }, - { - "$ref": "#/definitions/BrowserContext" - }, - { - "$ref": "#/definitions/GpuContext" - }, - { - "$ref": "#/definitions/TraceContext" - }, - { - "$ref": "#/definitions/MonitorContext" - }, - { - "type": "object", - "additionalProperties": true - } - ] - }, - "ContextInner": { - "anyOf": [ - { - "$ref": "#/definitions/Context" - } - ] - }, - "Contexts": { - "description": " The Contexts Interface provides additional context data. Typically, this is data related to the\n current user and the environment. For example, the device or application version. Its canonical\n name is `contexts`.\n\n The `contexts` type can be used to define arbitrary contextual data on the event. It accepts an\n object of key/value pairs. The key is the “alias” of the context and can be freely chosen.\n However, as per policy, it should match the type of the context unless there are two values for\n a type. You can omit `type` if the key name is the type.\n\n Unknown data for the contexts is rendered as a key/value list.\n\n For more details about sending additional data with your event, see the [full documentation on\n Additional Data](https://docs.sentry.io/enriching-error-data/additional-data/).", - "anyOf": [ - { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "$ref": "#/definitions/ContextInner" - }, - { - "type": "null" - } - ] - } - } - ] - }, - "Cookies": { - "description": " A map holding cookies.", - "anyOf": [ - { - "anyOf": [ - { - "type": "object", - "additionalProperties": { - "type": [ - "string", - "null" - ] - } - }, - { - "type": "array", - "items": { - "type": [ - "array", - "null" - ], - "items": [ - { - "type": [ - "string", - "null" - ] - }, - { - "type": [ - "string", - "null" - ] - } - ], - "maxItems": 2, - "minItems": 2 - } - } - ] - } - ] - }, - "DebugId": { - "type": "string" - }, - "DebugImage": { - "description": " A debug information file (debug image).", - "anyOf": [ - { - "$ref": "#/definitions/AppleDebugImage" - }, - { - "$ref": "#/definitions/NativeDebugImage" - }, - { - "$ref": "#/definitions/NativeDebugImage" - }, - { - "$ref": "#/definitions/NativeDebugImage" - }, - { - "$ref": "#/definitions/NativeDebugImage" - }, - { - "$ref": "#/definitions/ProguardDebugImage" - }, - { - "$ref": "#/definitions/NativeDebugImage" - }, - { - "type": "object", - "additionalProperties": true - } - ] - }, - "DebugMeta": { - "description": " Debugging and processing meta information.\n\n The debug meta interface carries debug information for processing errors and crash reports.\n Sentry amends the information in this interface.\n\n Example (look at field types to see more detail):\n\n ```json\n {\n \"debug_meta\": {\n \"images\": [],\n \"sdk_info\": {\n \"sdk_name\": \"iOS\",\n \"version_major\": 10,\n \"version_minor\": 3,\n \"version_patchlevel\": 0\n }\n }\n }\n ```", - "anyOf": [ - { - "type": "object", - "properties": { - "images": { - "description": " List of debug information files (debug images).", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "anyOf": [ - { - "$ref": "#/definitions/DebugImage" - }, - { - "type": "null" - } - ] - } - }, - "sdk_info": { - "description": " Information about the system SDK (e.g. iOS SDK).", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/SystemSdkInfo" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - ] - }, - "DeviceContext": { - "description": " Device information.\n\n Device context describes the device that caused the event. This is most appropriate for mobile\n applications.", - "anyOf": [ - { - "type": "object", - "properties": { - "arch": { - "description": " Native cpu architecture of the device.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "battery_level": { - "description": " Current battery level in %.\n\n If the device has a battery, this can be a floating point value defining the battery level\n (in the range 0-100).", - "default": null, - "type": [ - "number", - "null" - ], - "format": "double" - }, - "boot_time": { - "description": " Indicator when the device was booted.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "brand": { - "description": " Brand of the device.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "charging": { - "description": " Whether the device was charging or not.", - "default": null, - "type": [ - "boolean", - "null" - ] - }, - "external_free_storage": { - "description": " Free size of the attached external storage in bytes (eg: android SDK card).", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "external_storage_size": { - "description": " Total size of the attached external storage in bytes (eg: android SDK card).", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "family": { - "description": " Family of the device model.\n\n This is usually the common part of model names across generations. For instance, `iPhone`\n would be a reasonable family, so would be `Samsung Galaxy`.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "free_memory": { - "description": " How much memory is still available in bytes.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "free_storage": { - "description": " How much storage is free in bytes.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "low_memory": { - "description": " Whether the device was low on memory.", - "default": null, - "type": [ - "boolean", - "null" - ] - }, - "manufacturer": { - "description": " Manufacturer of the device.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "memory_size": { - "description": " Total memory available in bytes.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "model": { - "description": " Device model.\n\n This, for example, can be `Samsung Galaxy S3`.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "model_id": { - "description": " Device model (internal identifier).\n\n An internal hardware revision to identify the device exactly.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "name": { - "description": " Name of the device.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "online": { - "description": " Whether the device was online or not.", - "default": null, - "type": [ - "boolean", - "null" - ] - }, - "orientation": { - "description": " Current screen orientation.\n\n This can be a string `portrait` or `landscape` to define the orientation of a device.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "screen_density": { - "description": " Device screen density.", - "default": null, - "type": [ - "number", - "null" - ], - "format": "double" - }, - "screen_dpi": { - "description": " Screen density as dots-per-inch.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "screen_resolution": { - "description": " Device screen resolution.\n\n (e.g.: 800x600, 3040x1444)", - "default": null, - "type": [ - "string", - "null" - ] - }, - "simulator": { - "description": " Simulator/prod indicator.", - "default": null, - "type": [ - "boolean", - "null" - ] - }, - "storage_size": { - "description": " Total storage size of the device in bytes.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "timezone": { - "description": " Timezone of the device.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "usable_memory": { - "description": " How much memory is usable for the app in bytes.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - ] - }, - "EventId": { - "description": " Wrapper around a UUID with slightly different formatting.", - "anyOf": [ - { - "type": "string", - "format": "uuid" - } - ] - }, - "EventProcessingError": { - "description": " An event processing error.", - "anyOf": [ - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "name": { - "description": " Affected key or deep path.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "type": { - "description": " The error kind.", - "type": [ - "string", - "null" - ] - }, - "value": { - "description": " The original value causing this error.", - "default": null - } - }, - "additionalProperties": false - } - ] - }, - "EventType": { - "description": "The type of an event.\n\nThe event type determines how Sentry handles the event and has an impact on processing, rate limiting, and quotas. There are three fundamental classes of event types:\n\n- **Error monitoring events** (`default`, `error`): Processed and grouped into unique issues based on their exception stack traces and error messages. - **Security events** (`csp`, `hpkp`, `expectct`, `expectstaple`): Derived from Browser security violation reports and grouped into unique issues based on the endpoint and violation. SDKs do not send such events. - **Transaction events** (`transaction`): Contain operation spans and collected into traces for performance monitoring.", - "type": "string", - "enum": [ - "error", - "csp", - "hpkp", - "expectct", - "expectstaple", - "transaction", - "default" - ] - }, - "Exception": { - "description": " A single exception.\n\n Multiple values inside of an [event](#typedef-Event) represent chained exceptions and should be sorted oldest to newest. For example, consider this Python code snippet:\n\n ```python\n try:\n raise Exception(\"random boring invariant was not met!\")\n except Exception as e:\n raise ValueError(\"something went wrong, help!\") from e\n ```\n\n `Exception` would be described first in the values list, followed by a description of `ValueError`:\n\n ```json\n {\n \"exception\": {\n \"values\": [\n {\"type\": \"Exception\": \"value\": \"random boring invariant was not met!\"},\n {\"type\": \"ValueError\", \"value\": \"something went wrong, help!\"},\n ]\n }\n }\n ```", - "anyOf": [ - { - "type": "object", - "properties": { - "mechanism": { - "description": " Mechanism by which this exception was generated and handled.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Mechanism" - }, - { - "type": "null" - } - ] - }, - "module": { - "description": " The optional module, or package which the exception type lives in.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "stacktrace": { - "description": " Stack trace containing frames of this exception.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Stacktrace" - }, - { - "type": "null" - } - ] - }, - "thread_id": { - "description": " An optional value that refers to a [thread](#typedef-Thread).", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/ThreadId" - }, - { - "type": "null" - } - ] - }, - "type": { - "description": " Exception type, e.g. `ValueError`.\n\n At least one of `type` or `value` is required, otherwise the exception is discarded.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "value": { - "description": " Human readable display value.\n\n At least one of `type` or `value` is required, otherwise the exception is discarded.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/JsonLenientString" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - ] - }, - "Fingerprint": { - "description": " A fingerprint value.", - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "Frame": { - "description": " Holds information about a single stacktrace frame.\n\n Each object should contain **at least** a `filename`, `function` or `instruction_addr` attribute. All values are optional, but recommended.", - "anyOf": [ - { - "type": "object", - "properties": { - "abs_path": { - "description": " Absolute path to the source file.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/NativeImagePath" - }, - { - "type": "null" - } - ] - }, - "addr_mode": { - "description": " Defines the addressing mode for addresses.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "colno": { - "description": " Column number within the source file, starting at 1.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "context_line": { - "description": " Source code of the current line (`lineno`).", - "default": null, - "type": [ - "string", - "null" - ] - }, - "filename": { - "description": " The source file name (basename only).", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/NativeImagePath" - }, - { - "type": "null" - } - ] - }, - "function": { - "description": " Name of the frame's function. This might include the name of a class.\n\n This function name may be shortened or demangled. If not, Sentry will demangle and shorten\n it for some platforms. The original function name will be stored in `raw_function`.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "image_addr": { - "description": " (C/C++/Native) Start address of the containing code module (image).", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - }, - "in_app": { - "description": " Override whether this frame should be considered part of application code, or part of\n libraries/frameworks/dependencies.\n\n Setting this attribute to `false` causes the frame to be hidden/collapsed by default and\n mostly ignored during issue grouping.", - "default": null, - "type": [ - "boolean", - "null" - ] - }, - "instruction_addr": { - "description": " (C/C++/Native) An optional instruction address for symbolication.\n\n This should be a string with a hexadecimal number that includes a 0x prefix.\n If this is set and a known image is defined in the\n [Debug Meta Interface]({%- link _documentation/development/sdk-dev/event-payloads/debugmeta.md -%}),\n then symbolication can take place.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - }, - "lineno": { - "description": " Line number within the source file, starting at 1.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "module": { - "description": " Name of the module the frame is contained in.\n\n Note that this might also include a class name if that is something the\n language natively considers to be part of the stack (for instance in Java).", - "default": null, - "type": [ - "string", - "null" - ] - }, - "package": { - "description": " Name of the package that contains the frame.\n\n For instance this can be a dylib for native languages, the name of the jar\n or .NET assembly.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "platform": { - "description": " Which platform this frame is from.\n\n This can override the platform for a single frame. Otherwise, the platform of the event is\n assumed. This can be used for multi-platform stack traces, such as in React Native.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "post_context": { - "description": " Source code of the lines after `lineno`.", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "type": [ - "string", - "null" - ] - } - }, - "pre_context": { - "description": " Source code leading up to `lineno`.", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "type": [ - "string", - "null" - ] - } - }, - "raw_function": { - "description": " A raw (but potentially truncated) function value.\n\n The original function name, if the function name is shortened or demangled. Sentry shows\n the raw function when clicking on the shortened one in the UI.\n\n If this has the same value as `function` it's best to be omitted. This\n exists because on many platforms the function itself contains additional\n information like overload specifies or a lot of generics which can make\n it exceed the maximum limit we provide for the field. In those cases\n then we cannot reliably trim down the function any more at a later point\n because the more valuable information has been removed.\n\n The logic to be applied is that an intelligently trimmed function name\n should be stored in `function` and the value before trimming is stored\n in this field instead. However also this field will be capped at 256\n characters at the moment which often means that not the entire original\n value can be stored.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "stack_start": { - "description": " Marks this frame as the bottom of a chained stack trace.\n\n Stack traces from asynchronous code consist of several sub traces that are chained together\n into one large list. This flag indicates the root function of a chained stack trace.\n Depending on the runtime and thread, this is either the `main` function or a thread base\n stub.\n\n This field should only be specified when true.", - "default": null, - "type": [ - "boolean", - "null" - ] - }, - "symbol": { - "description": " Potentially mangled name of the symbol as it appears in an executable.\n\n This is different from a function name by generally being the mangled\n name that appears natively in the binary. This is relevant for languages\n like Swift, C++ or Rust.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "symbol_addr": { - "description": " (C/C++/Native) Start address of the frame's function.\n\n We use the instruction address for symbolication, but this can be used to calculate\n an instruction offset automatically.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - }, - "vars": { - "description": " Mapping of local variables and expression names that were available in this frame.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/FrameVars" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - ] - }, - "FrameVars": { - "description": " Frame local variables.", - "anyOf": [ - { - "type": "object", - "additionalProperties": true - } - ] - }, - "Geo": { - "description": " Geographical location of the end user or device.", - "anyOf": [ - { - "type": "object", - "properties": { - "city": { - "description": " Human readable city name.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "country_code": { - "description": " Two-letter country code (ISO 3166-1 alpha-2).", - "default": null, - "type": [ - "string", - "null" - ] - }, - "region": { - "description": " Human readable region name or code.", - "default": null, - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - ] - }, - "GpuContext": { - "description": " GPU information.\n\n Example:\n\n ```json\n \"gpu\": {\n \"name\": \"AMD Radeon Pro 560\",\n \"vendor_name\": \"Apple\",\n \"memory_size\": 4096,\n \"api_type\": \"Metal\",\n \"multi_threaded_rendering\": true,\n \"version\": \"Metal\",\n \"npot_support\": \"Full\"\n }\n ```", - "anyOf": [ - { - "type": "object", - "properties": { - "api_type": { - "description": " The device low-level API type.\n\n Examples: `\"Apple Metal\"` or `\"Direct3D11\"`", - "default": null, - "type": [ - "string", - "null" - ] - }, - "id": { - "description": " The PCI identifier of the graphics device.", - "default": null - }, - "memory_size": { - "description": " The total GPU memory available in Megabytes.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "multi_threaded_rendering": { - "description": " Whether the GPU has multi-threaded rendering or not.", - "default": null, - "type": [ - "boolean", - "null" - ] - }, - "name": { - "description": " The name of the graphics device.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "npot_support": { - "description": " The Non-Power-Of-Two support.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "vendor_id": { - "description": " The PCI vendor identifier of the graphics device.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "vendor_name": { - "description": " The vendor name as reported by the graphics device.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "version": { - "description": " The Version of the graphics device.", - "default": null, - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - ] - }, - "HeaderName": { - "description": " A \"into-string\" type that normalizes header names.", - "anyOf": [ - { - "type": "string" - } - ] - }, - "HeaderValue": { - "description": " A \"into-string\" type that normalizes header values.", - "anyOf": [ - { - "type": "string" - } - ] - }, - "Headers": { - "description": " A map holding headers.", - "anyOf": [ - { - "anyOf": [ - { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "$ref": "#/definitions/HeaderValue" - }, - { - "type": "null" - } - ] - } - }, - { - "type": "array", - "items": { - "type": [ - "array", - "null" - ], - "items": [ - { - "anyOf": [ - { - "$ref": "#/definitions/HeaderName" - }, - { - "type": "null" - } - ] - }, - { - "anyOf": [ - { - "$ref": "#/definitions/HeaderValue" - }, - { - "type": "null" - } - ] - } - ], - "maxItems": 2, - "minItems": 2 - } - } - ] - } - ] - }, - "JsonLenientString": { - "description": " A \"into-string\" type of value. All non-string values are serialized as JSON.", - "anyOf": [ - { - "type": "string" - } - ] - }, - "Level": { - "description": "Severity level of an event or breadcrumb.", - "type": "string", - "enum": [ - "debug", - "info", - "warning", - "error", - "fatal" - ] - }, - "LogEntry": { - "description": " A log entry message.\n\n A log message is similar to the `message` attribute on the event itself but\n can additionally hold optional parameters.\n\n ```json\n {\n \"message\": {\n \"message\": \"My raw message with interpreted strings like %s\",\n \"params\": [\"this\"]\n }\n }\n ```\n\n ```json\n {\n \"message\": {\n \"message\": \"My raw message with interpreted strings like {foo}\",\n \"params\": {\"foo\": \"this\"}\n }\n }\n ```", - "anyOf": [ - { - "type": "object", - "properties": { - "formatted": { - "description": " The formatted message. If `message` and `params` are given, Sentry\n will attempt to backfill `formatted` if empty.\n\n It must not exceed 8192 characters. Longer messages will be truncated.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Message" - }, - { - "type": "null" - } - ] - }, - "message": { - "description": " The log message with parameter placeholders.\n\n This attribute is primarily used for grouping related events together into issues.\n Therefore this really should just be a string template, i.e. `Sending %d requests` instead\n of `Sending 9999 requests`. The latter is much better at home in `formatted`.\n\n It must not exceed 8192 characters. Longer messages will be truncated.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Message" - }, - { - "type": "null" - } - ] - }, - "params": { - "description": " Parameters to be interpolated into the log message. This can be an array of positional\n parameters as well as a mapping of named arguments to their values.", - "default": null - } - }, - "additionalProperties": false - } - ] - }, - "MachException": { - "description": " Mach exception information.", - "anyOf": [ - { - "type": "object", - "properties": { - "code": { - "description": " The mach exception code.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "exception": { - "description": " The mach exception type.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "int64" - }, - "name": { - "description": " Optional name of the mach exception.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "subcode": { - "description": " The mach exception subcode.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - ] - }, - "Mechanism": { - "description": " The mechanism by which an exception was generated and handled.\n\n The exception mechanism is an optional field residing in the [exception](#typedef-Exception).\n It carries additional information about the way the exception was created on the target system.\n This includes general exception values obtained from the operating system or runtime APIs, as\n well as mechanism-specific values.", - "anyOf": [ - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "data": { - "description": " Arbitrary extra data that might help the user understand the error thrown by this mechanism.", - "default": null, - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "description": { - "description": " Optional human-readable description of the error mechanism.\n\n May include a possible hint on how to solve this error.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "handled": { - "description": " Flag indicating whether this exception was handled.\n\n This is a best-effort guess at whether the exception was handled by user code or not. For\n example:\n\n - Exceptions leading to a 500 Internal Server Error or to a hard process crash are\n `handled=false`, as the SDK typically has an integration that automatically captures the\n error.\n\n - Exceptions captured using `capture_exception` (called from user code) are `handled=true`\n as the user explicitly captured the exception (and therefore kind of handled it)", - "default": null, - "type": [ - "boolean", - "null" - ] - }, - "help_link": { - "description": " Link to online resources describing this error.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "meta": { - "description": " Operating system or runtime meta information.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/MechanismMeta" - }, - { - "type": "null" - } - ] - }, - "synthetic": { - "description": " If this is set then the exception is not a real exception but some\n form of synthetic error for instance from a signal handler, a hard\n segfault or similar where type and value are not useful for grouping\n or display purposes.", - "default": null, - "type": [ - "boolean", - "null" - ] - }, - "type": { - "description": " Mechanism type (required).\n\n Required unique identifier of this mechanism determining rendering and processing of the\n mechanism data.\n\n In the Python SDK this is merely the name of the framework integration that produced the\n exception, while for native it is e.g. `\"minidump\"` or `\"applecrashreport\"`.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - ] - }, - "MechanismMeta": { - "description": " Operating system or runtime meta information to an exception mechanism.\n\n The mechanism metadata usually carries error codes reported by the runtime or operating system,\n along with a platform-dependent interpretation of these codes. SDKs can safely omit code names\n and descriptions for well-known error codes, as it will be filled out by Sentry. For\n proprietary or vendor-specific error codes, adding these values will give additional\n information to the user.", - "anyOf": [ - { - "type": "object", - "properties": { - "errno": { - "description": " Optional ISO C standard error code.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/CError" - }, - { - "type": "null" - } - ] - }, - "mach_exception": { - "description": " A Mach Exception on Apple systems comprising a code triple and optional descriptions.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/MachException" - }, - { - "type": "null" - } - ] - }, - "ns_error": { - "description": " An NSError on Apple systems comprising code and signal.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/NsError" - }, - { - "type": "null" - } - ] - }, - "signal": { - "description": " Information on the POSIX signal.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/PosixSignal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - ] - }, - "Message": { - "anyOf": [ - { - "type": "string" - } - ] - }, - "MonitorContext": { - "description": " Monitor information.", - "anyOf": [ - { - "type": "object", - "additionalProperties": true - } - ] - }, - "NativeDebugImage": { - "description": " A generic (new-style) native platform debug information file.\n\n The `type` key must be one of:\n\n - `macho`\n - `elf`: ELF images are used on Linux platforms. Their structure is identical to other native images.\n - `pe`\n\n Examples:\n\n ```json\n {\n \"type\": \"elf\",\n \"code_id\": \"68220ae2c65d65c1b6aaa12fa6765a6ec2f5f434\",\n \"code_file\": \"/lib/x86_64-linux-gnu/libgcc_s.so.1\",\n \"debug_id\": \"e20a2268-5dc6-c165-b6aa-a12fa6765a6e\",\n \"image_addr\": \"0x7f5140527000\",\n \"image_size\": 90112,\n \"image_vmaddr\": \"0x40000\",\n \"arch\": \"x86_64\"\n }\n ```\n\n ```json\n {\n \"type\": \"pe\",\n \"code_id\": \"57898e12145000\",\n \"code_file\": \"C:\\\\Windows\\\\System32\\\\dbghelp.dll\",\n \"debug_id\": \"9c2a902b-6fdf-40ad-8308-588a41d572a0-1\",\n \"debug_file\": \"dbghelp.pdb\",\n \"image_addr\": \"0x70850000\",\n \"image_size\": \"1331200\",\n \"image_vmaddr\": \"0x40000\",\n \"arch\": \"x86\"\n }\n ```\n\n ```json\n {\n \"type\": \"macho\",\n \"debug_id\": \"84a04d24-0e60-3810-a8c0-90a65e2df61a\",\n \"debug_file\": \"libDiagnosticMessagesClient.dylib\",\n \"code_file\": \"/usr/lib/libDiagnosticMessagesClient.dylib\",\n \"image_addr\": \"0x7fffe668e000\",\n \"image_size\": 8192,\n \"image_vmaddr\": \"0x40000\",\n \"arch\": \"x86_64\",\n }\n ```", - "anyOf": [ - { - "type": "object", - "required": [ - "code_file", - "debug_id" - ], - "properties": { - "arch": { - "description": " CPU architecture target.\n\n Architecture of the module. If missing, this will be backfilled by Sentry.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "code_file": { - "description": " Path and name of the image file (required).\n\n The absolute path to the dynamic library or executable. This helps to locate the file if it is missing on Sentry.\n\n - `pe`: The code file should be provided to allow server-side stack walking of binary crash reports, such as Minidumps.", - "anyOf": [ - { - "$ref": "#/definitions/NativeImagePath" - }, - { - "type": "null" - } - ] - }, - "code_id": { - "description": " Optional identifier of the code file.\n\n - `elf`: If the program was compiled with a relatively recent compiler, this should be the hex representation of the `NT_GNU_BUILD_ID` program header (type `PT_NOTE`), or the value of the `.note.gnu.build-id` note section (type `SHT_NOTE`). Otherwise, leave this value empty.\n\n Certain symbol servers use the code identifier to locate debug information for ELF images, in which case this field should be included if possible.\n\n - `pe`: Identifier of the executable or DLL. It contains the values of the `time_date_stamp` from the COFF header and `size_of_image` from the optional header formatted together into a hex string using `%08x%X` (note that the second value is not padded):\n\n ```text\n time_date_stamp: 0x5ab38077\n size_of_image: 0x9000\n code_id: 5ab380779000\n ```\n\n The code identifier should be provided to allow server-side stack walking of binary crash reports, such as Minidumps.\n\n\n - `macho`: Identifier of the dynamic library or executable. It is the value of the `LC_UUID` load command in the Mach header, formatted as UUID. Can be empty for Mach images, as it is equivalent to the debug identifier.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/CodeId" - }, - { - "type": "null" - } - ] - }, - "debug_file": { - "description": " Path and name of the debug companion file.\n\n - `elf`: Name or absolute path to the file containing stripped debug information for this image. This value might be _required_ to retrieve debug files from certain symbol servers.\n\n - `pe`: Name of the PDB file containing debug information for this image. This value is often required to retrieve debug files from specific symbol servers.\n\n - `macho`: Name or absolute path to the dSYM file containing debug information for this image. This value might be required to retrieve debug files from certain symbol servers.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/NativeImagePath" - }, - { - "type": "null" - } - ] - }, - "debug_id": { - "description": " Unique debug identifier of the image.\n\n - `elf`: Debug identifier of the dynamic library or executable. If a code identifier is available, the debug identifier is the little-endian UUID representation of the first 16-bytes of that\n identifier. Spaces are inserted for readability, note the byte order of the first fields:\n\n ```text\n code id: f1c3bcc0 2798 65fe 3058 404b2831d9e6 4135386c\n debug id: c0bcc3f1-9827-fe65-3058-404b2831d9e6\n ```\n\n If no code id is available, the debug id should be computed by XORing the first 4096 bytes of the `.text` section in 16-byte chunks, and representing it as a little-endian UUID (again swapping the byte order).\n\n - `pe`: `signature` and `age` of the PDB file. Both values can be read from the CodeView PDB70 debug information header in the PE. The value should be represented as little-endian UUID, with the age appended at the end. Note that the byte order of the UUID fields must be swapped (spaces inserted for readability):\n\n ```text\n signature: f1c3bcc0 2798 65fe 3058 404b2831d9e6\n age: 1\n debug_id: c0bcc3f1-9827-fe65-3058-404b2831d9e6-1\n ```\n\n - `macho`: Identifier of the dynamic library or executable. It is the value of the `LC_UUID` load command in the Mach header, formatted as UUID.", - "anyOf": [ - { - "$ref": "#/definitions/DebugId" - }, - { - "type": "null" - } - ] - }, - "image_addr": { - "description": " Starting memory address of the image (required).\n\n Memory address, at which the image is mounted in the virtual address space of the process. Should be a string in hex representation prefixed with `\"0x\"`.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - }, - "image_size": { - "description": " Size of the image in bytes (required).\n\n The size of the image in virtual memory. If missing, Sentry will assume that the image spans up to the next image, which might lead to invalid stack traces.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "image_vmaddr": { - "description": " Loading address in virtual memory.\n\n Preferred load address of the image in virtual memory, as declared in the headers of the\n image. When loading an image, the operating system may still choose to place it at a\n different address.\n\n Symbols and addresses in the native image are always relative to the start of the image and do not consider the preferred load address. It is merely a hint to the loader.\n\n - `elf`/`macho`: If this value is non-zero, all symbols and addresses declared in the native image start at this address, rather than 0. By contrast, Sentry deals with addresses relative to the start of the image. For example, with `image_vmaddr: 0x40000`, a symbol located at `0x401000` has a relative address of `0x1000`.\n\n Relative addresses used in Apple Crash Reports and `addr2line` are usually in the preferred address space, and not relative address space.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - ] - }, - "NativeImagePath": { - "description": " A type for strings that are generally paths, might contain system user names, but still cannot\n be stripped liberally because it would break processing for certain platforms.\n\n Those strings get special treatment in our PII processor to avoid stripping the basename.", - "anyOf": [ - { - "type": "string" - } - ] - }, - "NsError": { - "description": " NSError informaiton.", - "anyOf": [ - { - "type": "object", - "properties": { - "code": { - "description": " The error code.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "int64" - }, - "domain": { - "description": " A string containing the error domain.", - "default": null, - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - ] - }, - "OsContext": { - "description": " Operating system information.\n\n OS context describes the operating system on which the event was created. In web contexts, this\n is the operating system of the browser (generally pulled from the User-Agent string).", - "anyOf": [ - { - "type": "object", - "properties": { - "build": { - "description": " Internal build number of the operating system.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "kernel_version": { - "description": " Current kernel version.\n\n This is typically the entire output of the `uname` syscall.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "name": { - "description": " Name of the operating system.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "raw_description": { - "description": " Unprocessed operating system info.\n\n An unprocessed description string obtained by the operating system. For some well-known\n runtimes, Sentry will attempt to parse `name` and `version` from this string, if they are\n not explicitly given.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "rooted": { - "description": " Indicator if the OS is rooted (mobile mostly).", - "default": null, - "type": [ - "boolean", - "null" - ] - }, - "version": { - "description": " Version of the operating system.", - "default": null, - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - ] - }, - "PosixSignal": { - "description": " POSIX signal with optional extended data.\n\n On Apple systems, signals also carry a code in addition to the signal number describing the\n signal in more detail. On Linux, this code does not exist.", - "anyOf": [ - { - "type": "object", - "properties": { - "code": { - "description": " An optional signal code present on Apple systems.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "int64" - }, - "code_name": { - "description": " Optional name of the errno constant.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "name": { - "description": " Optional name of the errno constant.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "number": { - "description": " The POSIX signal number.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "int64" - } - }, - "additionalProperties": false - } - ] - }, - "ProguardDebugImage": { - "description": " Proguard mapping file.\n\n Proguard images refer to `mapping.txt` files generated when Proguard obfuscates function names. The Java SDK integrations assign this file a unique identifier, which has to be included in the list of images.", - "anyOf": [ - { - "type": "object", - "required": [ - "uuid" - ], - "properties": { - "uuid": { - "description": " UUID computed from the file contents, assigned by the Java SDK.", - "type": [ - "string", - "null" - ], - "format": "uuid" - } - }, - "additionalProperties": false - } - ] - }, - "RawStacktrace": { - "description": " A stack trace of a single thread.\n\n A stack trace contains a list of frames, each with various bits (most optional) describing the context of that frame. Frames should be sorted from oldest to newest.\n\n For the given example program written in Python:\n\n ```python\n def foo():\n my_var = 'foo'\n raise ValueError()\n\n def main():\n foo()\n ```\n\n A minimalistic stack trace for the above program in the correct order:\n\n ```json\n {\n \"frames\": [\n {\"function\": \"main\"},\n {\"function\": \"foo\"}\n ]\n }\n ```\n\n The top frame fully symbolicated with five lines of source context:\n\n ```json\n {\n \"frames\": [{\n \"in_app\": true,\n \"function\": \"myfunction\",\n \"abs_path\": \"/real/file/name.py\",\n \"filename\": \"file/name.py\",\n \"lineno\": 3,\n \"vars\": {\n \"my_var\": \"'value'\"\n },\n \"pre_context\": [\n \"def foo():\",\n \" my_var = 'foo'\",\n ],\n \"context_line\": \" raise ValueError()\",\n \"post_context\": [\n \"\",\n \"def main():\"\n ],\n }]\n }\n ```\n\n A minimal native stack trace with register values. Note that the `package` event attribute must be \"native\" for these frames to be symbolicated.\n\n ```json\n {\n \"frames\": [\n {\"instruction_addr\": \"0x7fff5bf3456c\"},\n {\"instruction_addr\": \"0x7fff5bf346c0\"},\n ],\n \"registers\": {\n \"rip\": \"0x00007ff6eef54be2\",\n \"rsp\": \"0x0000003b710cd9e0\"\n }\n }\n ```", - "anyOf": [ - { - "type": "object", - "required": [ - "frames" - ], - "properties": { - "frames": { - "description": " Required. A non-empty list of stack frames. The list is ordered from caller to callee, or oldest to youngest. The last frame is the one creating the exception.", - "type": [ - "array", - "null" - ], - "items": { - "anyOf": [ - { - "$ref": "#/definitions/Frame" - }, - { - "type": "null" - } - ] - } - }, - "lang": { - "description": " The language of the stacktrace.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "registers": { - "description": " Register values of the thread (top frame).\n\n A map of register names and their values. The values should contain the actual register values of the thread, thus mapping to the last frame in the list.", - "default": null, - "type": [ - "object", - "null" - ], - "additionalProperties": { - "anyOf": [ - { - "$ref": "#/definitions/RegVal" - }, - { - "type": "null" - } - ] - } - }, - "snapshot": { - "description": " Indicates that this stack trace is a snapshot triggered by an external signal.\n\n If this field is `false`, then the stack trace points to the code that caused this stack\n trace to be created. This can be the location of a raised exception, as well as an exception\n or signal handler.\n\n If this field is `true`, then the stack trace was captured as part of creating an unrelated\n event. For example, a thread other than the crashing thread, or a stack trace computed as a\n result of an external kill signal.", - "default": null, - "type": [ - "boolean", - "null" - ] - } - }, - "additionalProperties": false - } - ] - }, - "RegVal": { - "type": "string" - }, - "Request": { - "description": " Http request information.\n\n The Request interface contains information on a HTTP request related to the event. In client\n SDKs, this can be an outgoing request, or the request that rendered the current web page. On\n server SDKs, this could be the incoming web request that is being handled.\n\n The data variable should only contain the request body (not the query string). It can either be\n a dictionary (for standard HTTP requests) or a raw request body.\n\n ### Ordered Maps\n\n In the Request interface, several attributes can either be declared as string, object, or list\n of tuples. Sentry attempts to parse structured information from the string representation in\n such cases.\n\n Sometimes, keys can be declared multiple times, or the order of elements matters. In such\n cases, use the tuple representation over a plain object.\n\n Example of request headers as object:\n\n ```json\n {\n \"content-type\": \"application/json\",\n \"accept\": \"application/json, application/xml\"\n }\n ```\n\n Example of the same headers as list of tuples:\n\n ```json\n [\n [\"content-type\", \"application/json\"],\n [\"accept\", \"application/json\"],\n [\"accept\", \"application/xml\"]\n ]\n ```\n\n Example of a fully populated request object:\n\n ```json\n {\n \"request\": {\n \"method\": \"POST\",\n \"url\": \"http://absolute.uri/foo\",\n \"query_string\": \"query=foobar&page=2\",\n \"data\": {\n \"foo\": \"bar\"\n },\n \"cookies\": \"PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;\",\n \"headers\": {\n \"content-type\": \"text/html\"\n },\n \"env\": {\n \"REMOTE_ADDR\": \"192.168.0.1\"\n }\n }\n }\n ```", - "anyOf": [ - { - "type": "object", - "properties": { - "cookies": { - "description": " The cookie values.\n\n Can be given unparsed as string, as dictionary, or as a list of tuples.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Cookies" - }, - { - "type": "null" - } - ] - }, - "data": { - "description": " Request data in any format that makes sense.\n\n SDKs should discard large and binary bodies by default. Can be given as string or\n structural data of any format.", - "default": null - }, - "env": { - "description": " Server environment data, such as CGI/WSGI.\n\n A dictionary containing environment information passed from the server. This is where\n information such as CGI/WSGI/Rack keys go that are not HTTP headers.\n\n Sentry will explicitly look for `REMOTE_ADDR` to extract an IP address.", - "default": null, - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "fragment": { - "description": " The fragment of the request URL.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "headers": { - "description": " A dictionary of submitted headers.\n\n If a header appears multiple times it, needs to be merged according to the HTTP standard\n for header merging. Header names are treated case-insensitively by Sentry.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Headers" - }, - { - "type": "null" - } - ] - }, - "inferred_content_type": { - "description": " The inferred content type of the request payload.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "method": { - "description": " HTTP request method.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "query_string": { - "description": " The query string component of the URL.\n\n Can be given as unparsed string, dictionary, or list of tuples.\n\n If the query string is not declared and part of the `url`, Sentry moves it to the\n query string.", - "default": null, - "anyOf": [ - { - "anyOf": [ - { - "type": "string" - }, - { - "anyOf": [ - { - "type": "object", - "additionalProperties": { - "type": [ - "string", - "null" - ] - } - }, - { - "type": "array", - "items": { - "type": [ - "array", - "null" - ], - "items": [ - { - "type": [ - "string", - "null" - ] - }, - { - "type": [ - "string", - "null" - ] - } - ], - "maxItems": 2, - "minItems": 2 - } - } - ] - } - ] - }, - { - "type": "null" - } - ] - }, - "url": { - "description": " The URL of the request if available.\n\nThe query string can be declared either as part of the `url`, or separately in `query_string`.", - "default": null, - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - ] - }, - "RuntimeContext": { - "description": " Runtime information.\n\n Runtime context describes a runtime in more detail. Typically, this context is present in\n `contexts` multiple times if multiple runtimes are involved (for instance, if you have a\n JavaScript application running on top of JVM).", - "anyOf": [ - { - "type": "object", - "properties": { - "build": { - "description": " Application build string, if it is separate from the version.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "name": { - "description": " Runtime name.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "raw_description": { - "description": " Unprocessed runtime info.\n\n An unprocessed description string obtained by the runtime. For some well-known runtimes,\n Sentry will attempt to parse `name` and `version` from this string, if they are not\n explicitly given.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "version": { - "description": " Runtime version string.", - "default": null, - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - ] - }, - "SpanId": { - "description": " A 16-character hex string as described in the W3C trace context spec.", - "anyOf": [ - { - "type": "string" - } - ] - }, - "SpanStatus": { - "description": "Trace status.\n\nValues from Mapping to HTTP from ", - "type": "string", - "enum": [ - "ok", - "cancelled", - "unknown", - "invalid_argument", - "deadline_exceeded", - "not_found", - "already_exists", - "permission_denied", - "resource_exhausted", - "failed_precondition", - "aborted", - "out_of_range", - "unimplemented", - "internal_error", - "unavailable", - "data_loss", - "unauthenticated" - ] - }, - "Stacktrace": { - "anyOf": [ - { - "$ref": "#/definitions/RawStacktrace" - } - ] - }, - "String": { - "type": "string" - }, - "SystemSdkInfo": { - "description": " Holds information about the system SDK.\n\n This is relevant for iOS and other platforms that have a system\n SDK. Not to be confused with the client SDK.", - "anyOf": [ - { - "type": "object", - "properties": { - "sdk_name": { - "description": " The internal name of the SDK.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "version_major": { - "description": " The major version of the SDK as integer or 0.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "version_minor": { - "description": " The minor version of the SDK as integer or 0.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "version_patchlevel": { - "description": " The patch version of the SDK as integer or 0.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - ] - }, - "TagEntry": { - "anyOf": [ - { - "type": "array", - "items": [ - { - "type": [ - "string", - "null" - ] - }, - { - "type": [ - "string", - "null" - ] - } - ], - "maxItems": 2, - "minItems": 2 - } - ] - }, - "Tags": { - "description": " Manual key/value tag pairs.", - "anyOf": [ - { - "anyOf": [ - { - "type": "object", - "additionalProperties": { - "type": [ - "string", - "null" - ] - } - }, - { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/definitions/TagEntry" - }, - { - "type": "null" - } - ] - } - } - ] - } - ] - }, - "Thread": { - "description": " A process thread of an event.\n\n The Threads Interface specifies threads that were running at the time an event happened. These threads can also contain stack traces.\n\n An event may contain one or more threads in an attribute named `threads`.\n\n The following example illustrates the threads part of the event payload and omits other attributes for simplicity.\n\n ```json\n {\n \"threads\": {\n \"values\": [\n {\n \"id\": \"0\",\n \"name\": \"main\",\n \"crashed\": true,\n \"stacktrace\": {}\n }\n ]\n }\n }\n ```", - "anyOf": [ - { - "type": "object", - "properties": { - "crashed": { - "description": " A flag indicating whether the thread crashed. Defaults to `false`.", - "default": null, - "type": [ - "boolean", - "null" - ] - }, - "current": { - "description": " A flag indicating whether the thread was in the foreground. Defaults to `false`.", - "default": null, - "type": [ - "boolean", - "null" - ] - }, - "id": { - "description": " The ID of the thread. Typically a number or numeric string.\n\n Needs to be unique among the threads. An exception can set the `thread_id` attribute to cross-reference this thread.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/ThreadId" - }, - { - "type": "null" - } - ] - }, - "name": { - "description": " Display name of this thread.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "stacktrace": { - "description": " Stack trace containing frames of this exception.\n\n The thread that crashed with an exception should not have a stack trace, but instead, the `thread_id` attribute should be set on the exception and Sentry will connect the two.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Stacktrace" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - ] - }, - "ThreadId": { - "description": " Represents a thread id.", - "anyOf": [ - { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - { - "type": "string" - } - ] - }, - "Timestamp": { - "description": "Can be a ISO-8601 formatted string or a unix timestamp in seconds (floating point values allowed).\n\nMust be UTC.", - "anyOf": [ - { - "type": "number", - "format": "double" - }, - { - "type": "string" - } - ] - }, - "TraceContext": { - "description": " Trace context", - "anyOf": [ - { - "type": "object", - "required": [ - "span_id", - "trace_id" - ], - "properties": { - "exclusive_time": { - "description": " The amount of time in milliseconds spent in this transaction span,\n excluding its immediate child spans.", - "default": null, - "type": [ - "number", - "null" - ], - "format": "double" - }, - "op": { - "description": " Span type (see `OperationType` docs).", - "default": null, - "type": [ - "string", - "null" - ] - }, - "parent_span_id": { - "description": " The ID of the span enclosing this span.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/SpanId" - }, - { - "type": "null" - } - ] - }, - "span_id": { - "description": " The ID of the span.", - "anyOf": [ - { - "$ref": "#/definitions/SpanId" - }, - { - "type": "null" - } - ] - }, - "status": { - "description": " Whether the trace failed or succeeded. Currently only used to indicate status of individual\n transactions.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/SpanStatus" - }, - { - "type": "null" - } - ] - }, - "trace_id": { - "description": " The trace ID.", - "anyOf": [ - { - "$ref": "#/definitions/TraceId" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - ] - }, - "TraceId": { - "description": " A 32-character hex string as described in the W3C trace context spec.", - "anyOf": [ - { - "type": "string" - } - ] - }, - "User": { - "description": " Information about the user who triggered an event.\n\n ```json\n {\n \"user\": {\n \"id\": \"unique_id\",\n \"username\": \"my_user\",\n \"email\": \"foo@example.com\",\n \"ip_address\": \"127.0.0.1\",\n \"subscription\": \"basic\"\n }\n }\n ```", - "anyOf": [ - { - "type": "object", - "properties": { - "data": { - "description": " Additional arbitrary fields, as stored in the database (and sometimes as sent by clients).\n All data from `self.other` should end up here after store normalization.", - "default": null, - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "email": { - "description": " Email address of the user.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "geo": { - "description": " Approximate geographical location of the end user or device.", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Geo" - }, - { - "type": "null" - } - ] - }, - "id": { - "description": " Unique identifier of the user.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "ip_address": { - "description": " Remote IP address of the user. Defaults to \"{{auto}}\".", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/String" - }, - { - "type": "null" - } - ] - }, - "name": { - "description": " Human readable name of the user.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "segment": { - "description": " The user segment, for apps that divide users in user segments.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "username": { - "description": " Username of the user.", - "default": null, - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - ] - } - } -} From 061122807adc666386c6e42389227e3f7b949aa6 Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Wed, 1 Jun 2022 13:46:42 -0700 Subject: [PATCH 14/24] rename payloads -> recordings --- CHANGELOG.md | 2 +- relay-config/src/config.rs | 10 ++-- relay-server/src/actors/envelopes.rs | 2 +- relay-server/src/actors/store.rs | 55 +++++++++---------- relay-server/src/envelope.rs | 8 +-- relay-server/src/utils/multipart.rs | 4 +- relay-server/src/utils/rate_limits.rs | 2 +- relay-server/src/utils/sizes.rs | 2 +- tests/integration/conftest.py | 2 +- tests/integration/fixtures/processing.py | 10 ++-- tests/integration/test_replay_payloads.py | 60 --------------------- tests/integration/test_replay_recordings.py | 60 +++++++++++++++++++++ 12 files changed, 109 insertions(+), 108 deletions(-) delete mode 100644 tests/integration/test_replay_payloads.py create mode 100644 tests/integration/test_replay_recordings.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c816aa350f2..3fb8a041245 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,7 +43,7 @@ - Refactor aggregation error, recover from errors more gracefully. ([#1240](https://github.com/getsentry/relay/pull/1240)) - Remove/reject nul-bytes from metric strings. ([#1235](https://github.com/getsentry/relay/pull/1235)) - Remove the unused "internal" data category. ([#1245](https://github.com/getsentry/relay/pull/1245)) -- Add ReplayPayload ItemType. ([#1236](https://github.com/getsentry/relay/pull/1236)) +- Add ReplayRecording ItemType. ([#1236](https://github.com/getsentry/relay/pull/1236)) - Add the client and version as `sdk` tag to extracted session metrics in the format `name/version`. ([#1248](https://github.com/getsentry/relay/pull/1248)) - Expose `shutdown_timeout` in `OverridableConfig` ([#1247](https://github.com/getsentry/relay/pull/1247)) - Normalize all profiles and reject invalid ones. ([#1250](https://github.com/getsentry/relay/pull/1250)) diff --git a/relay-config/src/config.rs b/relay-config/src/config.rs index 4eabe8b1314..450ae67a1d4 100644 --- a/relay-config/src/config.rs +++ b/relay-config/src/config.rs @@ -779,8 +779,8 @@ pub enum KafkaTopic { Metrics, /// Profiles Profiles, - /// ReplayPayloads, large blobs sent by the replay sdk - ReplayPayloads, + /// ReplayRecording, large blobs sent by the replay sdk + ReplayRecordings, } /// Configuration for topics. @@ -804,7 +804,7 @@ pub struct TopicAssignments { /// Stacktrace topic name pub profiles: TopicAssignment, /// Payloads topic name. - pub replay_payloads: TopicAssignment, + pub replay_recordings: TopicAssignment, } impl TopicAssignments { @@ -819,7 +819,7 @@ impl TopicAssignments { KafkaTopic::Sessions => &self.sessions, KafkaTopic::Metrics => &self.metrics, KafkaTopic::Profiles => &self.profiles, - KafkaTopic::ReplayPayloads => &self.replay_payloads, + KafkaTopic::ReplayRecordings => &self.replay_recordings, } } } @@ -835,7 +835,7 @@ impl Default for TopicAssignments { sessions: "ingest-sessions".to_owned().into(), metrics: "ingest-metrics".to_owned().into(), profiles: "profiles".to_owned().into(), - replay_payloads: "ingest-replay-payloads".to_owned().into(), + replay_recordings: "ingest-replay-recordings".to_owned().into(), } } } diff --git a/relay-server/src/actors/envelopes.rs b/relay-server/src/actors/envelopes.rs index de07af6510e..ca9ebc84627 100644 --- a/relay-server/src/actors/envelopes.rs +++ b/relay-server/src/actors/envelopes.rs @@ -1320,7 +1320,7 @@ impl EnvelopeProcessor { ItemType::MetricBuckets => false, ItemType::ClientReport => false, ItemType::Profile => false, - ItemType::ReplayPayload => false, + ItemType::ReplayRecording => false, // Without knowing more, `Unknown` items are allowed to be repeated ItemType::Unknown(_) => false, } diff --git a/relay-server/src/actors/store.rs b/relay-server/src/actors/store.rs index 1af4717d361..071f6ffbd5c 100644 --- a/relay-server/src/actors/store.rs +++ b/relay-server/src/actors/store.rs @@ -59,7 +59,7 @@ struct Producers { sessions: Producer, metrics: Producer, profiles: Producer, - replay_payloads: Producer, + replay_recordings: Producer, } impl Producers { @@ -77,7 +77,7 @@ impl Producers { KafkaTopic::Sessions => Some(&self.sessions), KafkaTopic::Metrics => Some(&self.metrics), KafkaTopic::Profiles => Some(&self.profiles), - KafkaTopic::ReplayPayloads => Some(&self.replay_payloads), + KafkaTopic::ReplayRecordings => Some(&self.replay_recordings), } } } @@ -135,10 +135,10 @@ impl StoreForwarder { sessions: make_producer(&*config, &mut reused_producers, KafkaTopic::Sessions)?, metrics: make_producer(&*config, &mut reused_producers, KafkaTopic::Metrics)?, profiles: make_producer(&*config, &mut reused_producers, KafkaTopic::Profiles)?, - replay_payloads: make_producer( + replay_recordings: make_producer( &*config, &mut reused_producers, - KafkaTopic::ReplayPayloads, + KafkaTopic::ReplayRecordings, )?, }; @@ -633,7 +633,7 @@ enum KafkaMessage { Session(SessionKafkaMessage), Metric(MetricKafkaMessage), Profile(ProfileKafkaMessage), - ReplayPayload(AttachmentKafkaMessage), + ReplayRecording(AttachmentKafkaMessage), } impl KafkaMessage { @@ -646,7 +646,7 @@ impl KafkaMessage { KafkaMessage::Session(_) => "session", KafkaMessage::Metric(_) => "metric", KafkaMessage::Profile(_) => "profile", - KafkaMessage::ReplayPayload(_) => "replay_payload", + KafkaMessage::ReplayRecording(_) => "replay_recording", } } @@ -660,7 +660,7 @@ impl KafkaMessage { Self::Session(_message) => Uuid::nil(), // Explicit random partitioning for sessions Self::Metric(_message) => Uuid::nil(), // TODO(ja): Determine a partitioning key Self::Profile(_message) => Uuid::nil(), - Self::ReplayPayload(_message) => Uuid::nil(), + Self::ReplayRecording(_message) => Uuid::nil(), }; if uuid.is_nil() { @@ -703,8 +703,8 @@ fn is_slow_item(item: &Item) -> bool { item.ty() == &ItemType::Attachment || item.ty() == &ItemType::UserReport } -fn is_replay_payload(item: &Item) -> bool { - item.ty() == &ItemType::ReplayPayload +fn is_replay_recording(item: &Item) -> bool { + item.ty() == &ItemType::ReplayRecording } impl Handler for StoreForwarder { @@ -731,14 +731,14 @@ impl Handler for StoreForwarder { KafkaTopic::Attachments } else if event_item.map(|x| x.ty()) == Some(&ItemType::Transaction) { KafkaTopic::Transactions - } else if envelope.get_item_by(is_replay_payload).is_some() { - KafkaTopic::ReplayPayloads + } else if envelope.get_item_by(is_replay_recording).is_some() { + KafkaTopic::ReplayRecordings } else { KafkaTopic::Events }; let mut attachments = Vec::new(); - let mut replay_payloads = Vec::new(); + let mut replay_recordings = Vec::new(); for item in envelope.items() { match item.ty() { @@ -784,15 +784,15 @@ impl Handler for StoreForwarder { start_time, item, )?, - ItemType::ReplayPayload => { - debug_assert!(topic == KafkaTopic::ReplayPayloads); - let replay_payload = self.produce_attachment_chunks( + ItemType::ReplayRecording => { + debug_assert!(topic == KafkaTopic::ReplayRecordings); + let replay_recording = self.produce_attachment_chunks( event_id.ok_or(StoreError::NoEventId)?, scoping.project_id, item, topic, )?; - replay_payloads.push(replay_payload); + replay_recordings.push(replay_recording); } _ => {} } @@ -829,19 +829,20 @@ impl Handler for StoreForwarder { event_type = "attachment" ); } - } else if !replay_payloads.is_empty() { - relay_log::trace!("Sending individual replay_payloads of envelope to kafka"); - for attachment in replay_payloads { - let replay_payload_message = KafkaMessage::ReplayPayload(AttachmentKafkaMessage { - event_id: event_id.ok_or(StoreError::NoEventId)?, - project_id: scoping.project_id, - attachment, - }); - - self.produce(KafkaTopic::ReplayPayloads, replay_payload_message)?; + } else if !replay_recordings.is_empty() { + relay_log::trace!("Sending individual replay_recordings of envelope to kafka"); + for attachment in replay_recordings { + let replay_recording_message = + KafkaMessage::ReplayRecording(AttachmentKafkaMessage { + event_id: event_id.ok_or(StoreError::NoEventId)?, + project_id: scoping.project_id, + attachment, + }); + + self.produce(KafkaTopic::ReplayRecordings, replay_recording_message)?; metric!( counter(RelayCounters::ProcessingMessageProduced) += 1, - event_type = "replay_payload" + event_type = "replay_recording" ); } } diff --git a/relay-server/src/envelope.rs b/relay-server/src/envelope.rs index b6026954e78..c7103cf3e66 100644 --- a/relay-server/src/envelope.rs +++ b/relay-server/src/envelope.rs @@ -104,7 +104,7 @@ pub enum ItemType { /// Profile event payload encoded in JSON Profile, /// Replay Payload blob payload - ReplayPayload, + ReplayRecording, /// 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 @@ -144,7 +144,7 @@ impl fmt::Display for ItemType { Self::MetricBuckets => write!(f, "metric_buckets"), Self::ClientReport => write!(f, "client_report"), Self::Profile => write!(f, "profile"), - Self::ReplayPayload => write!(f, "replay_payload"), + Self::ReplayRecording => write!(f, "replay_recording"), Self::Unknown(s) => s.fmt(f), } } @@ -624,7 +624,7 @@ impl Item { | ItemType::Metrics | ItemType::MetricBuckets | ItemType::ClientReport - | ItemType::ReplayPayload + | ItemType::ReplayRecording | ItemType::Profile => false, // The unknown item type can observe any behavior, most likely there are going to be no @@ -651,7 +651,7 @@ impl Item { ItemType::Metrics => false, ItemType::MetricBuckets => false, ItemType::ClientReport => false, - ItemType::ReplayPayload => false, + ItemType::ReplayRecording => false, ItemType::Profile => true, // Since this Relay cannot interpret the semantics of this item, it does not know diff --git a/relay-server/src/utils/multipart.rs b/relay-server/src/utils/multipart.rs index f4e4f48c7e3..b6ba6066be4 100644 --- a/relay-server/src/utils/multipart.rs +++ b/relay-server/src/utils/multipart.rs @@ -12,7 +12,7 @@ use crate::envelope::{AttachmentType, ContentType, Item, ItemType, Items}; use crate::extractors::DecodingPayload; use crate::service::ServiceState; -const REPLAY_RECORDING_FILENAME: &str = "sentry_replay_payload"; +const REPLAY_RECORDING_FILENAME: &str = "sentry_replay_recording"; #[derive(Debug, Fail)] pub enum MultipartError { @@ -221,7 +221,7 @@ fn consume_item( if let Some(file_name) = file_name { let item_type = match file_name { - REPLAY_RECORDING_FILENAME => ItemType::ReplayPayload, + REPLAY_RECORDING_FILENAME => ItemType::ReplayRecording, _ => ItemType::Attachment, }; let mut item = Item::new(item_type); diff --git a/relay-server/src/utils/rate_limits.rs b/relay-server/src/utils/rate_limits.rs index 9ec7616131d..8836d4be423 100644 --- a/relay-server/src/utils/rate_limits.rs +++ b/relay-server/src/utils/rate_limits.rs @@ -104,7 +104,7 @@ fn infer_event_category(item: &Item) -> Option { ItemType::FormData => None, ItemType::UserReport => None, ItemType::Profile => None, - ItemType::ReplayPayload => None, + ItemType::ReplayRecording => None, ItemType::ClientReport => None, ItemType::Unknown(_) => None, } diff --git a/relay-server/src/utils/sizes.rs b/relay-server/src/utils/sizes.rs index 33307877949..227360bc32e 100644 --- a/relay-server/src/utils/sizes.rs +++ b/relay-server/src/utils/sizes.rs @@ -27,7 +27,7 @@ pub fn check_envelope_size_limits(config: &Config, envelope: &Envelope) -> bool | ItemType::Security | ItemType::RawSecurity | ItemType::FormData => event_size += item.len(), - ItemType::Attachment | ItemType::UnrealReport | ItemType::ReplayPayload => { + ItemType::Attachment | ItemType::UnrealReport | ItemType::ReplayRecording => { if item.len() > config.max_attachment_size() { return false; } diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index d534b42f20a..a4d9abb1c6e 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -22,7 +22,7 @@ outcomes_consumer, transactions_consumer, attachments_consumer, - replay_payloads_consumer, + replay_recordings_consumer, sessions_consumer, metrics_consumer, ) # noqa diff --git a/tests/integration/fixtures/processing.py b/tests/integration/fixtures/processing.py index 96493b61ff3..9eea86cbdab 100644 --- a/tests/integration/fixtures/processing.py +++ b/tests/integration/fixtures/processing.py @@ -51,7 +51,7 @@ def inner(options=None): "outcomes": get_topic_name("outcomes"), "sessions": get_topic_name("sessions"), "metrics": get_topic_name("metrics"), - "replay_payloads": get_topic_name("replay_payloads"), + "replay_recordings": get_topic_name("replay_recordings"), } if not processing.get("redis"): @@ -282,8 +282,8 @@ def metrics_consumer(kafka_consumer): @pytest.fixture -def replay_payloads_consumer(kafka_consumer): - return lambda: ReplayPayloadsConsumer(*kafka_consumer("replay_payloads")) +def replay_recordings_consumer(kafka_consumer): + return lambda: ReplayRecordingsConsumer(*kafka_consumer("replay_recordings")) class MetricsConsumer(ConsumerBase): @@ -342,7 +342,7 @@ def get_individual_attachment(self): return v -class ReplayPayloadsConsumer(EventsConsumer): +class ReplayRecordingsConsumer(EventsConsumer): def get_replay_chunk(self): message = self.poll() assert message is not None @@ -358,5 +358,5 @@ def get_individual_replay(self): assert message.error() is None v = msgpack.unpackb(message.value(), raw=False, use_list=False) - assert v["type"] == "replay_payload", v["type"] + assert v["type"] == "replay_recording", v["type"] return v diff --git a/tests/integration/test_replay_payloads.py b/tests/integration/test_replay_payloads.py deleted file mode 100644 index 10ee4a9e879..00000000000 --- a/tests/integration/test_replay_payloads.py +++ /dev/null @@ -1,60 +0,0 @@ -import pytest -import time -import uuid - -from requests.exceptions import HTTPError - - -def test_payload( - mini_sentry, relay_with_processing, replay_payloads_consumer, outcomes_consumer -): - project_id = 42 - event_id = "515539018c9b4260a6f999572f1661ee" - - relay = relay_with_processing() - mini_sentry.add_full_project_config(project_id) - replay_payloads_consumer = replay_payloads_consumer() - outcomes_consumer = outcomes_consumer() - - replay_payloads = [ - ("sentry_replay_payload", "sentry_replay_payload", b"test"), - ] - relay.send_attachments(project_id, event_id, replay_payloads) - - replay_payload_contents = {} - replay_payload_ids = [] - replay_payload_num_chunks = {} - - while set(replay_payload_contents.values()) != {b"test"}: - chunk, v = replay_payloads_consumer.get_replay_chunk() - replay_payload_contents[v["id"]] = ( - replay_payload_contents.get(v["id"], b"") + chunk - ) - if v["id"] not in replay_payload_ids: - replay_payload_ids.append(v["id"]) - num_chunks = 1 + replay_payload_num_chunks.get(v["id"], 0) - assert v["chunk_index"] == num_chunks - 1 - replay_payload_num_chunks[v["id"]] = num_chunks - - id1 = replay_payload_ids[0] - - assert replay_payload_contents[id1] == b"test" - - replay_payload = replay_payloads_consumer.get_individual_replay() - - assert replay_payload == { - "type": "replay_payload", - "attachment": { - "attachment_type": "event.attachment", - "chunks": replay_payload_num_chunks[id1], - "content_type": "application/octet-stream", - "id": id1, - "name": "sentry_replay_payload", - "size": len(replay_payload_contents[id1]), - "rate_limited": False, - }, - "event_id": event_id, - "project_id": project_id, - } - - outcomes_consumer.assert_empty() diff --git a/tests/integration/test_replay_recordings.py b/tests/integration/test_replay_recordings.py new file mode 100644 index 00000000000..00ae9b958ac --- /dev/null +++ b/tests/integration/test_replay_recordings.py @@ -0,0 +1,60 @@ +import pytest +import time +import uuid + +from requests.exceptions import HTTPError + + +def test_payload( + mini_sentry, relay_with_processing, replay_recordings_consumer, outcomes_consumer +): + project_id = 42 + event_id = "515539018c9b4260a6f999572f1661ee" + + relay = relay_with_processing() + mini_sentry.add_full_project_config(project_id) + replay_recordings_consumer = replay_recordings_consumer() + outcomes_consumer = outcomes_consumer() + + replay_recordings = [ + ("sentry_replay_recording", "sentry_replay_recording", b"test"), + ] + relay.send_attachments(project_id, event_id, replay_recordings) + + replay_recording_contents = {} + replay_recording_ids = [] + replay_recording_num_chunks = {} + + while set(replay_recording_contents.values()) != {b"test"}: + chunk, v = replay_recordings_consumer.get_replay_chunk() + replay_recording_contents[v["id"]] = ( + replay_recording_contents.get(v["id"], b"") + chunk + ) + if v["id"] not in replay_recording_ids: + replay_recording_ids.append(v["id"]) + num_chunks = 1 + replay_recording_num_chunks.get(v["id"], 0) + assert v["chunk_index"] == num_chunks - 1 + replay_recording_num_chunks[v["id"]] = num_chunks + + id1 = replay_recording_ids[0] + + assert replay_recording_contents[id1] == b"test" + + replay_recording = replay_recordings_consumer.get_individual_replay() + + assert replay_recording == { + "type": "replay_recording", + "attachment": { + "attachment_type": "event.attachment", + "chunks": replay_recording_num_chunks[id1], + "content_type": "application/octet-stream", + "id": id1, + "name": "sentry_replay_recording", + "size": len(replay_recording_contents[id1]), + "rate_limited": False, + }, + "event_id": event_id, + "project_id": project_id, + } + + outcomes_consumer.assert_empty() From e62d36aeb8471fcd203e31070d29bfd41f039795 Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Wed, 1 Jun 2022 17:33:02 -0700 Subject: [PATCH 15/24] checkpoint: get envelope replay test working in principle --- relay-server/src/actors/envelopes.rs | 18 ++++ relay-server/src/actors/project.rs | 2 + relay-server/src/actors/store.rs | 107 +++++++++++++++----- relay-server/src/envelope.rs | 19 ++++ relay-server/src/utils/multipart.rs | 9 +- tests/integration/test_replay_recordings.py | 21 ++-- 6 files changed, 132 insertions(+), 44 deletions(-) diff --git a/relay-server/src/actors/envelopes.rs b/relay-server/src/actors/envelopes.rs index ca9ebc84627..487f99533bb 100644 --- a/relay-server/src/actors/envelopes.rs +++ b/relay-server/src/actors/envelopes.rs @@ -974,6 +974,7 @@ impl EnvelopeProcessor { /// Remove profiles if the feature flag is not enabled fn process_profiles(&self, state: &mut ProcessEnvelopeState) { let profiling_enabled = state.project_state.has_feature(Feature::Profiling); + let context = state.envelope_context; state.envelope.retain_items(|item| { match item.ty() { @@ -1006,6 +1007,22 @@ impl EnvelopeProcessor { }); } + /// Remove replay recordings if the feature flag is not enabled + fn process_replay_recordings(&self, state: &mut ProcessEnvelopeState) { + let replays_enabled = state.project_state.has_feature(Feature::Replays); + state.envelope.retain_items(|item| { + match item.ty() { + ItemType::ReplayRecording => { + if !replays_enabled { + return false; + } + return true; + } + _ => true, // Keep all other item types + } + }); + } + /// Creates and initializes the processing state. /// /// This applies defaults to the envelope and initializes empty rate limits. @@ -1834,6 +1851,7 @@ impl EnvelopeProcessor { self.process_client_reports(state); self.process_user_reports(state); self.process_profiles(state); + self.process_replay_recordings(state); if state.creates_event() { if_processing!({ diff --git a/relay-server/src/actors/project.rs b/relay-server/src/actors/project.rs index ebfc7fbbc3e..4c034cf18ea 100644 --- a/relay-server/src/actors/project.rs +++ b/relay-server/src/actors/project.rs @@ -54,6 +54,8 @@ pub enum Feature { /// Enables ingestion and normalization of profiles. #[serde(rename = "organizations:profiling")] Profiling, + #[serde(rename = "organizations:session-replay")] + Replays, /// Unused. /// diff --git a/relay-server/src/actors/store.rs b/relay-server/src/actors/store.rs index 071f6ffbd5c..ea57a384895 100644 --- a/relay-server/src/actors/store.rs +++ b/relay-server/src/actors/store.rs @@ -171,7 +171,6 @@ impl StoreForwarder { event_id: EventId, project_id: ProjectId, item: &Item, - topic: KafkaTopic, ) -> Result { let id = Uuid::new_v4().to_string(); @@ -193,7 +192,7 @@ impl StoreForwarder { id: id.clone(), chunk_index, }); - self.produce(topic, attachment_message)?; + self.produce(KafkaTopic::Attachments, attachment_message)?; offset += chunk_size; chunk_index += 1; } @@ -231,7 +230,7 @@ impl StoreForwarder { start_time: UnixTimestamp::from_instant(start_time).as_secs(), }); - self.produce(KafkaTopic::Attachments, message) + self.produce(KafkaTopic::ReplayRecordings, message) } fn produce_sessions( @@ -446,6 +445,56 @@ impl StoreForwarder { ); Ok(()) } + + fn produce_replay_recording_chunks( + &self, + event_id: EventId, + project_id: ProjectId, + item: &Item, + ) -> Result { + let id = Uuid::new_v4().to_string(); + + let mut chunk_index = 0; + let mut offset = 0; + let payload = item.payload(); + let size = item.len(); + + // This skips chunks for empty attachments. The consumer does not require chunks for + // empty attachments. `chunks` will be `0` in this case. + while offset < size { + let max_chunk_size = self.config.attachment_chunk_size(); + let chunk_size = std::cmp::min(max_chunk_size, size - offset); + + let attachment_message = KafkaMessage::AttachmentChunk(AttachmentChunkKafkaMessage { + payload: payload.slice(offset, offset + chunk_size), + event_id, + project_id, + id: id.clone(), + chunk_index, + }); + self.produce(KafkaTopic::ReplayRecordings, attachment_message)?; + offset += chunk_size; + chunk_index += 1; + } + + // The chunk_index is incremented after every loop iteration. After we exit the loop, it + // is one larger than the last chunk, so it is equal to the number of chunks. + + Ok(ChunkedAttachment { + id, + name: match item.filename() { + Some(name) => name.to_owned(), + None => UNNAMED_ATTACHMENT.to_owned(), + }, + content_type: item + .content_type() + .map(|content_type| content_type.as_str().to_owned()), + attachment_type: item.attachment_type().unwrap_or_default(), + chunks: chunk_index, + size: Some(size), + rate_limited: Some(item.rate_limited()), + }) + } } /// StoreMessageForwarder is an async actor since the only thing it does is put the messages @@ -565,6 +614,16 @@ struct AttachmentKafkaMessage { attachment: ChunkedAttachment, } +#[derive(Debug, Serialize)] +struct ReplayRecordingKafkaMessage { + /// The event id. + replay_id: EventId, + /// The project id for the current event. + project_id: ProjectId, + /// The recording. + recording: ChunkedAttachment, +} + /// User report for an event wrapped up in a message ready for consumption in Kafka. /// /// Is always independent of an event and can be sent as part of any envelope. @@ -633,7 +692,7 @@ enum KafkaMessage { Session(SessionKafkaMessage), Metric(MetricKafkaMessage), Profile(ProfileKafkaMessage), - ReplayRecording(AttachmentKafkaMessage), + ReplayRecording(ReplayRecordingKafkaMessage), } impl KafkaMessage { @@ -703,6 +762,7 @@ fn is_slow_item(item: &Item) -> bool { item.ty() == &ItemType::Attachment || item.ty() == &ItemType::UserReport } +#[inline] fn is_replay_recording(item: &Item) -> bool { item.ty() == &ItemType::ReplayRecording } @@ -731,14 +791,11 @@ impl Handler for StoreForwarder { KafkaTopic::Attachments } else if event_item.map(|x| x.ty()) == Some(&ItemType::Transaction) { KafkaTopic::Transactions - } else if envelope.get_item_by(is_replay_recording).is_some() { - KafkaTopic::ReplayRecordings } else { KafkaTopic::Events }; let mut attachments = Vec::new(); - let mut replay_recordings = Vec::new(); for item in envelope.items() { match item.ty() { @@ -748,7 +805,6 @@ impl Handler for StoreForwarder { event_id.ok_or(StoreError::NoEventId)?, scoping.project_id, item, - topic, )?; attachments.push(attachment); } @@ -785,15 +841,26 @@ impl Handler for StoreForwarder { item, )?, ItemType::ReplayRecording => { - debug_assert!(topic == KafkaTopic::ReplayRecordings); - let replay_recording = self.produce_attachment_chunks( + let replay_recording = self.produce_replay_recording_chunks( event_id.ok_or(StoreError::NoEventId)?, scoping.project_id, item, - topic, )?; - replay_recordings.push(replay_recording); + relay_log::trace!("Sending individual replay_recordings of envelope to kafka"); + let replay_recording_message = + KafkaMessage::ReplayRecording(ReplayRecordingKafkaMessage { + replay_id: event_id.ok_or(StoreError::NoEventId)?, + project_id: scoping.project_id, + recording: replay_recording, + }); + + self.produce(KafkaTopic::ReplayRecordings, replay_recording_message)?; + metric!( + counter(RelayCounters::ProcessingMessageProduced) += 1, + event_type = "replay_recording" + ); } + _ => {} } } @@ -829,22 +896,6 @@ impl Handler for StoreForwarder { event_type = "attachment" ); } - } else if !replay_recordings.is_empty() { - relay_log::trace!("Sending individual replay_recordings of envelope to kafka"); - for attachment in replay_recordings { - let replay_recording_message = - KafkaMessage::ReplayRecording(AttachmentKafkaMessage { - event_id: event_id.ok_or(StoreError::NoEventId)?, - project_id: scoping.project_id, - attachment, - }); - - self.produce(KafkaTopic::ReplayRecordings, replay_recording_message)?; - metric!( - counter(RelayCounters::ProcessingMessageProduced) += 1, - event_type = "replay_recording" - ); - } } Ok(()) diff --git a/relay-server/src/envelope.rs b/relay-server/src/envelope.rs index c7103cf3e66..b7f697f7b73 100644 --- a/relay-server/src/envelope.rs +++ b/relay-server/src/envelope.rs @@ -169,6 +169,7 @@ impl std::str::FromStr for ItemType { "metric_buckets" => Self::MetricBuckets, "client_report" => Self::ClientReport, "profile" => Self::Profile, + "replay_recording" => Self::ReplayRecording, other => Self::Unknown(other.to_owned()), }) } @@ -1368,6 +1369,24 @@ mod tests { assert_eq!(items[0].len(), 10); } + #[test] + fn test_deserialize_envelope_replay_recording() { + // TODO: expand on this test + // With terminating newline after item payload + let bytes = Bytes::from( + "\ + {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\",\"dsn\":\"https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42\"}\n\ + {\"type\":\"replay_recording\"}\n\ + helloworld\n\ + ", + ); + + let envelope = Envelope::parse_bytes(bytes).unwrap(); + assert_eq!(envelope.len(), 1); + let items: Vec<_> = envelope.items().collect(); + assert_eq!(items[1].ty(), &ItemType::ReplayRecording); + } + #[test] fn test_parse_request_envelope() { let bytes = Bytes::from("{\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}"); diff --git a/relay-server/src/utils/multipart.rs b/relay-server/src/utils/multipart.rs index b6ba6066be4..e2d9f70d331 100644 --- a/relay-server/src/utils/multipart.rs +++ b/relay-server/src/utils/multipart.rs @@ -12,8 +12,6 @@ use crate::envelope::{AttachmentType, ContentType, Item, ItemType, Items}; use crate::extractors::DecodingPayload; use crate::service::ServiceState; -const REPLAY_RECORDING_FILENAME: &str = "sentry_replay_recording"; - #[derive(Debug, Fail)] pub enum MultipartError { #[fail(display = "payload reached its size limit")] @@ -220,12 +218,7 @@ fn consume_item( let file_name = content_disposition.as_ref().and_then(|d| d.get_filename()); if let Some(file_name) = file_name { - let item_type = match file_name { - REPLAY_RECORDING_FILENAME => ItemType::ReplayRecording, - _ => ItemType::Attachment, - }; - let mut item = Item::new(item_type); - + let mut item = Item::new(ItemType::Attachment); item.set_attachment_type((*content.infer_type)(field_name)); item.set_payload(content_type.into(), data); item.set_filename(file_name); diff --git a/tests/integration/test_replay_recordings.py b/tests/integration/test_replay_recordings.py index 00ae9b958ac..2c977e6f130 100644 --- a/tests/integration/test_replay_recordings.py +++ b/tests/integration/test_replay_recordings.py @@ -3,6 +3,7 @@ import uuid from requests.exceptions import HTTPError +from sentry_sdk.envelope import Envelope, Item, PayloadRef def test_payload( @@ -12,20 +13,23 @@ def test_payload( event_id = "515539018c9b4260a6f999572f1661ee" relay = relay_with_processing() - mini_sentry.add_full_project_config(project_id) + mini_sentry.add_basic_project_config( + project_id, extra={"config": {"features": ["organizations:session-replay"]}} + ) replay_recordings_consumer = replay_recordings_consumer() outcomes_consumer = outcomes_consumer() - replay_recordings = [ - ("sentry_replay_recording", "sentry_replay_recording", b"test"), - ] - relay.send_attachments(project_id, event_id, replay_recordings) + envelope = Envelope(headers=[["event_id", event_id]]) + envelope.add_item(Item(payload=PayloadRef(bytes=b"test"), type="replay_recording")) + + relay.send_envelope(project_id, envelope) replay_recording_contents = {} replay_recording_ids = [] replay_recording_num_chunks = {} while set(replay_recording_contents.values()) != {b"test"}: + print(replay_recording_contents.values()) chunk, v = replay_recordings_consumer.get_replay_chunk() replay_recording_contents[v["id"]] = ( replay_recording_contents.get(v["id"], b"") + chunk @@ -41,19 +45,20 @@ def test_payload( assert replay_recording_contents[id1] == b"test" replay_recording = replay_recordings_consumer.get_individual_replay() + print(replay_recording) assert replay_recording == { "type": "replay_recording", - "attachment": { + "recording": { "attachment_type": "event.attachment", "chunks": replay_recording_num_chunks[id1], "content_type": "application/octet-stream", "id": id1, - "name": "sentry_replay_recording", + "name": "Unnamed Attachment", "size": len(replay_recording_contents[id1]), "rate_limited": False, }, - "event_id": event_id, + "replay_id": event_id, "project_id": project_id, } From 35a0755e90006aed90d567ece93adec1964c6d59 Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Wed, 1 Jun 2022 18:00:14 -0700 Subject: [PATCH 16/24] small fixups --- relay-general/src/store/normalize.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/relay-general/src/store/normalize.rs b/relay-general/src/store/normalize.rs index 7263fe4cfcb..f342eb88d9b 100644 --- a/relay-general/src/store/normalize.rs +++ b/relay-general/src/store/normalize.rs @@ -115,7 +115,7 @@ impl<'a> NormalizeProcessor<'a> { /// Ensure measurements interface is only present for transaction events fn normalize_measurements(&self, event: &mut Event) { if event.ty.value() != Some(&EventType::Transaction) { - // Only transaction/replay events may have a measurements interface + // Only transaction events may have a measurements interface event.measurements = Annotated::empty(); } } @@ -129,7 +129,7 @@ impl<'a> NormalizeProcessor<'a> { } fn normalize_spans(&self, event: &mut Event) { - if event.ty.value() != Some(&EventType::Transaction) { + if event.ty.value() == Some(&EventType::Transaction) { spans::normalize_spans(event, &self.config.span_attributes); } } From 404371f90453897cb9ef72ca38d273e378202a8b Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Thu, 2 Jun 2022 13:13:35 -0700 Subject: [PATCH 17/24] add second test for without processing --- relay-server/src/actors/store.rs | 2 +- tests/integration/test_replay_recordings.py | 27 ++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/relay-server/src/actors/store.rs b/relay-server/src/actors/store.rs index ea57a384895..fdd5c490e5a 100644 --- a/relay-server/src/actors/store.rs +++ b/relay-server/src/actors/store.rs @@ -719,7 +719,7 @@ impl KafkaMessage { Self::Session(_message) => Uuid::nil(), // Explicit random partitioning for sessions Self::Metric(_message) => Uuid::nil(), // TODO(ja): Determine a partitioning key Self::Profile(_message) => Uuid::nil(), - Self::ReplayRecording(_message) => Uuid::nil(), + Self::ReplayRecording(message) => message.replay_id.0, }; if uuid.is_nil() { diff --git a/tests/integration/test_replay_recordings.py b/tests/integration/test_replay_recordings.py index 2c977e6f130..2ffa5effee3 100644 --- a/tests/integration/test_replay_recordings.py +++ b/tests/integration/test_replay_recordings.py @@ -6,7 +6,32 @@ from sentry_sdk.envelope import Envelope, Item, PayloadRef -def test_payload( +def test_replay_recordings(mini_sentry, relay_chain): + relay = relay_chain() + + project_id = 42 + mini_sentry.add_basic_project_config( + project_id, extra={"config": {"features": ["organizations:session-replay"]}} + ) + + event_id = "515539018c9b4260a6f999572f1661ee" + + envelope = Envelope(headers=[["event_id", event_id]]) + envelope.add_item(Item(payload=PayloadRef(bytes=b"test"), type="replay_recording")) + + relay.send_envelope(project_id, envelope) + + envelope = mini_sentry.captured_events.get(timeout=1) + assert len(envelope.items) == 1 + + session_item = envelope.items[0] + assert session_item.type == "replay_recording" + + replay_recording = session_item.get_bytes() + assert replay_recording == b"test" + + +def test_replay_recordings_processing( mini_sentry, relay_with_processing, replay_recordings_consumer, outcomes_consumer ): project_id = 42 From 88b7cb7276d78429e612b726a6c603a1f3d66638 Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Thu, 2 Jun 2022 21:00:30 -0700 Subject: [PATCH 18/24] separate out recording chunks --- relay-server/src/actors/envelopes.rs | 16 +++---- relay-server/src/actors/store.rs | 48 +++++++++++++-------- relay-server/src/envelope.rs | 6 +-- tests/integration/fixtures/processing.py | 2 +- tests/integration/test_replay_recordings.py | 12 +++--- 5 files changed, 45 insertions(+), 39 deletions(-) diff --git a/relay-server/src/actors/envelopes.rs b/relay-server/src/actors/envelopes.rs index 487f99533bb..38cc086bfdf 100644 --- a/relay-server/src/actors/envelopes.rs +++ b/relay-server/src/actors/envelopes.rs @@ -974,7 +974,6 @@ impl EnvelopeProcessor { /// Remove profiles if the feature flag is not enabled fn process_profiles(&self, state: &mut ProcessEnvelopeState) { let profiling_enabled = state.project_state.has_feature(Feature::Profiling); - let context = state.envelope_context; state.envelope.retain_items(|item| { match item.ty() { @@ -1007,19 +1006,16 @@ impl EnvelopeProcessor { }); } - /// Remove replay recordings if the feature flag is not enabled fn process_replay_recordings(&self, state: &mut ProcessEnvelopeState) { let replays_enabled = state.project_state.has_feature(Feature::Replays); - state.envelope.retain_items(|item| { - match item.ty() { - ItemType::ReplayRecording => { - if !replays_enabled { - return false; - } - return true; + state.envelope.retain_items(|item| match item.ty() { + ItemType::ReplayRecording => { + if !replays_enabled { + return false; } - _ => true, // Keep all other item types + return true; } + _ => true, }); } diff --git a/relay-server/src/actors/store.rs b/relay-server/src/actors/store.rs index fdd5c490e5a..e74b015078c 100644 --- a/relay-server/src/actors/store.rs +++ b/relay-server/src/actors/store.rs @@ -184,7 +184,6 @@ impl StoreForwarder { while offset < size { let max_chunk_size = self.config.attachment_chunk_size(); let chunk_size = std::cmp::min(max_chunk_size, size - offset); - let attachment_message = KafkaMessage::AttachmentChunk(AttachmentChunkKafkaMessage { payload: payload.slice(offset, offset + chunk_size), event_id, @@ -230,7 +229,7 @@ impl StoreForwarder { start_time: UnixTimestamp::from_instant(start_time).as_secs(), }); - self.produce(KafkaTopic::ReplayRecordings, message) + self.produce(KafkaTopic::Attachments, message) } fn produce_sessions( @@ -448,7 +447,7 @@ impl StoreForwarder { fn produce_replay_recording_chunks( &self, - event_id: EventId, + replay_id: EventId, project_id: ProjectId, item: &Item, ) -> Result { @@ -459,19 +458,20 @@ impl StoreForwarder { let payload = item.payload(); let size = item.len(); - // This skips chunks for empty attachments. The consumer does not require chunks for - // empty attachments. `chunks` will be `0` in this case. + // This skips chunks for empty replay recordings. The consumer does not require chunks for + // empty replay recordings. `chunks` will be `0` in this case. while offset < size { let max_chunk_size = self.config.attachment_chunk_size(); let chunk_size = std::cmp::min(max_chunk_size, size - offset); - let attachment_message = KafkaMessage::AttachmentChunk(AttachmentChunkKafkaMessage { - payload: payload.slice(offset, offset + chunk_size), - event_id, - project_id, - id: id.clone(), - chunk_index, - }); + let attachment_message = + KafkaMessage::ReplayRecordingChunk(ReplayRecordingChunkKafkaMessage { + payload: payload.slice(offset, offset + chunk_size), + replay_id, + project_id, + id: id.clone(), + chunk_index, + }); self.produce(KafkaTopic::ReplayRecordings, attachment_message)?; offset += chunk_size; chunk_index += 1; @@ -614,6 +614,22 @@ struct AttachmentKafkaMessage { attachment: ChunkedAttachment, } +/// Container payload for chunks of attachments. +#[derive(Debug, Serialize)] +struct ReplayRecordingChunkKafkaMessage { + /// Chunk payload of the attachment. + payload: Bytes, + /// The replay id. + replay_id: EventId, + /// The project id for the current event. + project_id: ProjectId, + /// The replay ID within the event. + /// + /// The triple `(project_id, replay_id, id)` identifies a replay recording chunk uniquely. + id: String, + /// Sequence number of chunk. Starts at 0 and ends at `ReplayRecordingKafkaMessage.num_chunks - 1`. + chunk_index: usize, +} #[derive(Debug, Serialize)] struct ReplayRecordingKafkaMessage { /// The event id. @@ -693,6 +709,7 @@ enum KafkaMessage { Metric(MetricKafkaMessage), Profile(ProfileKafkaMessage), ReplayRecording(ReplayRecordingKafkaMessage), + ReplayRecordingChunk(ReplayRecordingChunkKafkaMessage), } impl KafkaMessage { @@ -706,6 +723,7 @@ impl KafkaMessage { KafkaMessage::Metric(_) => "metric", KafkaMessage::Profile(_) => "profile", KafkaMessage::ReplayRecording(_) => "replay_recording", + KafkaMessage::ReplayRecordingChunk(_) => "replay_recording_chunk", } } @@ -720,6 +738,7 @@ impl KafkaMessage { Self::Metric(_message) => Uuid::nil(), // TODO(ja): Determine a partitioning key Self::Profile(_message) => Uuid::nil(), Self::ReplayRecording(message) => message.replay_id.0, + Self::ReplayRecordingChunk(message) => message.replay_id.0, }; if uuid.is_nil() { @@ -762,11 +781,6 @@ fn is_slow_item(item: &Item) -> bool { item.ty() == &ItemType::Attachment || item.ty() == &ItemType::UserReport } -#[inline] -fn is_replay_recording(item: &Item) -> bool { - item.ty() == &ItemType::ReplayRecording -} - impl Handler for StoreForwarder { type Result = Result<(), StoreError>; diff --git a/relay-server/src/envelope.rs b/relay-server/src/envelope.rs index b7f697f7b73..50eec3b33cf 100644 --- a/relay-server/src/envelope.rs +++ b/relay-server/src/envelope.rs @@ -103,7 +103,7 @@ pub enum ItemType { ClientReport, /// Profile event payload encoded in JSON Profile, - /// Replay Payload blob payload + /// Replay Recording data ReplayRecording, /// A new item type that is yet unknown by this version of Relay. /// @@ -1371,8 +1371,6 @@ mod tests { #[test] fn test_deserialize_envelope_replay_recording() { - // TODO: expand on this test - // With terminating newline after item payload let bytes = Bytes::from( "\ {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\",\"dsn\":\"https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42\"}\n\ @@ -1384,7 +1382,7 @@ mod tests { let envelope = Envelope::parse_bytes(bytes).unwrap(); assert_eq!(envelope.len(), 1); let items: Vec<_> = envelope.items().collect(); - assert_eq!(items[1].ty(), &ItemType::ReplayRecording); + assert_eq!(items[0].ty(), &ItemType::ReplayRecording); } #[test] diff --git a/tests/integration/fixtures/processing.py b/tests/integration/fixtures/processing.py index 9eea86cbdab..f042c01f160 100644 --- a/tests/integration/fixtures/processing.py +++ b/tests/integration/fixtures/processing.py @@ -349,7 +349,7 @@ def get_replay_chunk(self): assert message.error() is None v = msgpack.unpackb(message.value(), raw=False, use_list=False) - assert v["type"] == "attachment_chunk", v["type"] + assert v["type"] == "replay_recording_chunk", v["type"] return v["payload"], v def get_individual_replay(self): diff --git a/tests/integration/test_replay_recordings.py b/tests/integration/test_replay_recordings.py index 2ffa5effee3..b37c6cf18bf 100644 --- a/tests/integration/test_replay_recordings.py +++ b/tests/integration/test_replay_recordings.py @@ -14,9 +14,9 @@ def test_replay_recordings(mini_sentry, relay_chain): project_id, extra={"config": {"features": ["organizations:session-replay"]}} ) - event_id = "515539018c9b4260a6f999572f1661ee" + replay_id = "515539018c9b4260a6f999572f1661ee" - envelope = Envelope(headers=[["event_id", event_id]]) + envelope = Envelope(headers=[["event_id", replay_id]]) envelope.add_item(Item(payload=PayloadRef(bytes=b"test"), type="replay_recording")) relay.send_envelope(project_id, envelope) @@ -35,7 +35,7 @@ def test_replay_recordings_processing( mini_sentry, relay_with_processing, replay_recordings_consumer, outcomes_consumer ): project_id = 42 - event_id = "515539018c9b4260a6f999572f1661ee" + replay_id = "515539018c9b4260a6f999572f1661ee" relay = relay_with_processing() mini_sentry.add_basic_project_config( @@ -44,7 +44,7 @@ def test_replay_recordings_processing( replay_recordings_consumer = replay_recordings_consumer() outcomes_consumer = outcomes_consumer() - envelope = Envelope(headers=[["event_id", event_id]]) + envelope = Envelope(headers=[["event_id", replay_id]]) envelope.add_item(Item(payload=PayloadRef(bytes=b"test"), type="replay_recording")) relay.send_envelope(project_id, envelope) @@ -54,7 +54,6 @@ def test_replay_recordings_processing( replay_recording_num_chunks = {} while set(replay_recording_contents.values()) != {b"test"}: - print(replay_recording_contents.values()) chunk, v = replay_recordings_consumer.get_replay_chunk() replay_recording_contents[v["id"]] = ( replay_recording_contents.get(v["id"], b"") + chunk @@ -70,7 +69,6 @@ def test_replay_recordings_processing( assert replay_recording_contents[id1] == b"test" replay_recording = replay_recordings_consumer.get_individual_replay() - print(replay_recording) assert replay_recording == { "type": "replay_recording", @@ -83,7 +81,7 @@ def test_replay_recordings_processing( "size": len(replay_recording_contents[id1]), "rate_limited": False, }, - "replay_id": event_id, + "replay_id": replay_id, "project_id": project_id, } From 3e3882c9d888ceb6315fabf58787f0e4c4923214 Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Thu, 2 Jun 2022 21:22:58 -0700 Subject: [PATCH 19/24] set defaults for recording attachment chunks --- relay-config/src/config.rs | 2 +- relay-server/src/actors/envelopes.rs | 2 +- relay-server/src/actors/store.rs | 25 ++++++++++----------- tests/integration/test_replay_recordings.py | 8 ++++--- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/relay-config/src/config.rs b/relay-config/src/config.rs index 450ae67a1d4..02d94a140aa 100644 --- a/relay-config/src/config.rs +++ b/relay-config/src/config.rs @@ -803,7 +803,7 @@ pub struct TopicAssignments { pub metrics: TopicAssignment, /// Stacktrace topic name pub profiles: TopicAssignment, - /// Payloads topic name. + /// Recordings topic name. pub replay_recordings: TopicAssignment, } diff --git a/relay-server/src/actors/envelopes.rs b/relay-server/src/actors/envelopes.rs index 38cc086bfdf..766325609fb 100644 --- a/relay-server/src/actors/envelopes.rs +++ b/relay-server/src/actors/envelopes.rs @@ -1013,7 +1013,7 @@ impl EnvelopeProcessor { if !replays_enabled { return false; } - return true; + true } _ => true, }); diff --git a/relay-server/src/actors/store.rs b/relay-server/src/actors/store.rs index e74b015078c..f3a1eb66b55 100644 --- a/relay-server/src/actors/store.rs +++ b/relay-server/src/actors/store.rs @@ -38,6 +38,8 @@ const MAX_EXPLODED_SESSIONS: usize = 100; /// Fallback name used for attachment items without a `filename` header. const UNNAMED_ATTACHMENT: &str = "Unnamed Attachment"; +const REPLAY_RECORDINGS_ATTACHMENT_NAME: &str = "rr"; + #[derive(Fail, Debug)] pub enum StoreError { #[fail(display = "failed to send kafka message")] @@ -464,7 +466,7 @@ impl StoreForwarder { let max_chunk_size = self.config.attachment_chunk_size(); let chunk_size = std::cmp::min(max_chunk_size, size - offset); - let attachment_message = + let replay_recording_chunk_message = KafkaMessage::ReplayRecordingChunk(ReplayRecordingChunkKafkaMessage { payload: payload.slice(offset, offset + chunk_size), replay_id, @@ -472,7 +474,7 @@ impl StoreForwarder { id: id.clone(), chunk_index, }); - self.produce(KafkaTopic::ReplayRecordings, attachment_message)?; + self.produce(KafkaTopic::ReplayRecordings, replay_recording_chunk_message)?; offset += chunk_size; chunk_index += 1; } @@ -482,10 +484,7 @@ impl StoreForwarder { Ok(ChunkedAttachment { id, - name: match item.filename() { - Some(name) => name.to_owned(), - None => UNNAMED_ATTACHMENT.to_owned(), - }, + name: REPLAY_RECORDINGS_ATTACHMENT_NAME.to_owned(), content_type: item .content_type() .map(|content_type| content_type.as_str().to_owned()), @@ -617,13 +616,13 @@ struct AttachmentKafkaMessage { /// Container payload for chunks of attachments. #[derive(Debug, Serialize)] struct ReplayRecordingChunkKafkaMessage { - /// Chunk payload of the attachment. + /// Chunk payload of the replay recording. payload: Bytes, /// The replay id. replay_id: EventId, - /// The project id for the current event. + /// The project id for the current replay. project_id: ProjectId, - /// The replay ID within the event. + /// The recording ID within the replay. /// /// The triple `(project_id, replay_id, id)` identifies a replay recording chunk uniquely. id: String, @@ -632,12 +631,12 @@ struct ReplayRecordingChunkKafkaMessage { } #[derive(Debug, Serialize)] struct ReplayRecordingKafkaMessage { - /// The event id. + /// The replay id. replay_id: EventId, /// The project id for the current event. project_id: ProjectId, - /// The recording. - recording: ChunkedAttachment, + /// The recording attachment. + replay_recording: ChunkedAttachment, } /// User report for an event wrapped up in a message ready for consumption in Kafka. @@ -865,7 +864,7 @@ impl Handler for StoreForwarder { KafkaMessage::ReplayRecording(ReplayRecordingKafkaMessage { replay_id: event_id.ok_or(StoreError::NoEventId)?, project_id: scoping.project_id, - recording: replay_recording, + replay_recording, }); self.produce(KafkaTopic::ReplayRecordings, replay_recording_message)?; diff --git a/tests/integration/test_replay_recordings.py b/tests/integration/test_replay_recordings.py index b37c6cf18bf..959426c23c5 100644 --- a/tests/integration/test_replay_recordings.py +++ b/tests/integration/test_replay_recordings.py @@ -44,7 +44,9 @@ def test_replay_recordings_processing( replay_recordings_consumer = replay_recordings_consumer() outcomes_consumer = outcomes_consumer() - envelope = Envelope(headers=[["event_id", replay_id]]) + envelope = Envelope( + headers=[["event_id", replay_id,], ["attachment_type", "replay_recording"]] + ) envelope.add_item(Item(payload=PayloadRef(bytes=b"test"), type="replay_recording")) relay.send_envelope(project_id, envelope) @@ -72,12 +74,12 @@ def test_replay_recordings_processing( assert replay_recording == { "type": "replay_recording", - "recording": { + "replay_recording": { "attachment_type": "event.attachment", "chunks": replay_recording_num_chunks[id1], "content_type": "application/octet-stream", "id": id1, - "name": "Unnamed Attachment", + "name": "rr", "size": len(replay_recording_contents[id1]), "rate_limited": False, }, From e86466614b47eb949e015c17d11e8f7e5896423b Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Tue, 7 Jun 2022 10:16:37 -0700 Subject: [PATCH 20/24] event_id -> replay_id --- relay-server/src/actors/store.rs | 6 +++--- tests/integration/test_replay_event.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/relay-server/src/actors/store.rs b/relay-server/src/actors/store.rs index 688d587da6e..cb60f131edc 100644 --- a/relay-server/src/actors/store.rs +++ b/relay-server/src/actors/store.rs @@ -456,13 +456,13 @@ impl StoreForwarder { } fn produce_replay_event( &self, - event_id: EventId, + replay_id: EventId, project_id: ProjectId, start_time: Instant, item: &Item, ) -> Result<(), StoreError> { let message = ReplayEventKafkaMessage { - event_id, + replay_id, project_id, start_time: UnixTimestamp::from_instant(start_time).as_secs(), payload: item.payload(), @@ -617,7 +617,7 @@ struct ReplayEventKafkaMessage { /// Time at which the event was received by Relay. start_time: u64, /// The event id. - event_id: EventId, + replay_id: EventId, /// The project id for the current event. project_id: ProjectId, } diff --git a/tests/integration/test_replay_event.py b/tests/integration/test_replay_event.py index ea32d9bab24..e4a0a22dc88 100644 --- a/tests/integration/test_replay_event.py +++ b/tests/integration/test_replay_event.py @@ -40,4 +40,3 @@ def test_replay_event(mini_sentry, relay_with_processing, replay_events_consumer ) assert "trace" in replay_event["contexts"] assert replay_event["seq_id"] == 0 - From 6240aacc7387c5933512e2358d473b50c092726b Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Thu, 9 Jun 2022 10:27:05 -0700 Subject: [PATCH 21/24] used ChunkedReplayRecording type --- CHANGELOG.md | 2 +- relay-server/src/actors/envelopes.rs | 7 +---- relay-server/src/actors/store.rs | 32 ++++++++++++--------- tests/integration/test_replay_recordings.py | 4 --- 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c81594ba9a0..71e5e10a329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Relay is now compatible with CentOS 7 and Red Hat Enterprise Linux 7 onward (kernel version _2.6.32_), depending on _glibc 2.17_ or newer. The `crash-handler` feature, which is currently enabled in the build published to DockerHub, additionally requires _curl 7.29_ or newer. ([#1279](https://github.com/getsentry/relay/pull/1279)) - Optionally start relay with `--upstream-dsn` to pass a Sentry DSN instead of the URL. This can be convenient when starting Relay in environments close to an SDK, where a DSN is already available. ([#1277](https://github.com/getsentry/relay/pull/1277)) - Add a new runtime mode `--aws-runtime-api=$AWS_LAMBDA_RUNTIME_API` that integrates Relay with the AWS Extensions API lifecycle. ([#1277](https://github.com/getsentry/relay/pull/1277)) +- Add ReplayRecording ItemType. ([#1236](https://github.com/getsentry/relay/pull/1236)) **Bug Fixes**: @@ -46,7 +47,6 @@ - Refactor aggregation error, recover from errors more gracefully. ([#1240](https://github.com/getsentry/relay/pull/1240)) - Remove/reject nul-bytes from metric strings. ([#1235](https://github.com/getsentry/relay/pull/1235)) - Remove the unused "internal" data category. ([#1245](https://github.com/getsentry/relay/pull/1245)) -- Add ReplayRecording ItemType. ([#1236](https://github.com/getsentry/relay/pull/1236)) - Add the client and version as `sdk` tag to extracted session metrics in the format `name/version`. ([#1248](https://github.com/getsentry/relay/pull/1248)) - Expose `shutdown_timeout` in `OverridableConfig` ([#1247](https://github.com/getsentry/relay/pull/1247)) - Normalize all profiles and reject invalid ones. ([#1250](https://github.com/getsentry/relay/pull/1250)) diff --git a/relay-server/src/actors/envelopes.rs b/relay-server/src/actors/envelopes.rs index 766325609fb..d92b3f56c63 100644 --- a/relay-server/src/actors/envelopes.rs +++ b/relay-server/src/actors/envelopes.rs @@ -1009,12 +1009,7 @@ impl EnvelopeProcessor { fn process_replay_recordings(&self, state: &mut ProcessEnvelopeState) { let replays_enabled = state.project_state.has_feature(Feature::Replays); state.envelope.retain_items(|item| match item.ty() { - ItemType::ReplayRecording => { - if !replays_enabled { - return false; - } - true - } + ItemType::ReplayRecording => replays_enabled, _ => true, }); } diff --git a/relay-server/src/actors/store.rs b/relay-server/src/actors/store.rs index f3a1eb66b55..6e3c8429509 100644 --- a/relay-server/src/actors/store.rs +++ b/relay-server/src/actors/store.rs @@ -38,8 +38,6 @@ const MAX_EXPLODED_SESSIONS: usize = 100; /// Fallback name used for attachment items without a `filename` header. const UNNAMED_ATTACHMENT: &str = "Unnamed Attachment"; -const REPLAY_RECORDINGS_ATTACHMENT_NAME: &str = "rr"; - #[derive(Fail, Debug)] pub enum StoreError { #[fail(display = "failed to send kafka message")] @@ -452,7 +450,7 @@ impl StoreForwarder { replay_id: EventId, project_id: ProjectId, item: &Item, - ) -> Result { + ) -> Result { let id = Uuid::new_v4().to_string(); let mut chunk_index = 0; @@ -482,16 +480,10 @@ impl StoreForwarder { // The chunk_index is incremented after every loop iteration. After we exit the loop, it // is one larger than the last chunk, so it is equal to the number of chunks. - Ok(ChunkedAttachment { + Ok(ChunkedReplayRecording { id, - name: REPLAY_RECORDINGS_ATTACHMENT_NAME.to_owned(), - content_type: item - .content_type() - .map(|content_type| content_type.as_str().to_owned()), - attachment_type: item.attachment_type().unwrap_or_default(), chunks: chunk_index, size: Some(size), - rate_limited: Some(item.rate_limited()), }) } } @@ -549,6 +541,21 @@ struct ChunkedAttachment { #[serde(skip_serializing_if = "Option::is_none")] rate_limited: Option, } +/// attributes for Replay Recordings +#[derive(Debug, Serialize)] +struct ChunkedReplayRecording { + /// The attachment ID within the event. + /// + /// The triple `(project_id, event_id, id)` identifies an attachment uniquely. + id: String, + + /// Number of chunks. Must be greater than zero. + chunks: usize, + + /// The size of the attachment in bytes. + #[serde(skip_serializing_if = "Option::is_none")] + size: Option, +} /// A hack to make rmp-serde behave more like serde-json when serializing enums. /// @@ -623,10 +630,9 @@ struct ReplayRecordingChunkKafkaMessage { /// The project id for the current replay. project_id: ProjectId, /// The recording ID within the replay. - /// - /// The triple `(project_id, replay_id, id)` identifies a replay recording chunk uniquely. id: String, /// Sequence number of chunk. Starts at 0 and ends at `ReplayRecordingKafkaMessage.num_chunks - 1`. + /// the tuple (id, chunk_index) is the unique identifier for a single chunk. chunk_index: usize, } #[derive(Debug, Serialize)] @@ -636,7 +642,7 @@ struct ReplayRecordingKafkaMessage { /// The project id for the current event. project_id: ProjectId, /// The recording attachment. - replay_recording: ChunkedAttachment, + replay_recording: ChunkedReplayRecording, } /// User report for an event wrapped up in a message ready for consumption in Kafka. diff --git a/tests/integration/test_replay_recordings.py b/tests/integration/test_replay_recordings.py index 959426c23c5..a122b6c92f9 100644 --- a/tests/integration/test_replay_recordings.py +++ b/tests/integration/test_replay_recordings.py @@ -75,13 +75,9 @@ def test_replay_recordings_processing( assert replay_recording == { "type": "replay_recording", "replay_recording": { - "attachment_type": "event.attachment", "chunks": replay_recording_num_chunks[id1], - "content_type": "application/octet-stream", "id": id1, - "name": "rr", "size": len(replay_recording_contents[id1]), - "rate_limited": False, }, "replay_id": replay_id, "project_id": project_id, From a8fd7424ce27b1b69d78bbfd15b50c40cd731911 Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Thu, 9 Jun 2022 10:31:38 -0700 Subject: [PATCH 22/24] specify to use min latest relay version --- tests/integration/test_replay_recordings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_replay_recordings.py b/tests/integration/test_replay_recordings.py index a122b6c92f9..b6890915798 100644 --- a/tests/integration/test_replay_recordings.py +++ b/tests/integration/test_replay_recordings.py @@ -7,7 +7,7 @@ def test_replay_recordings(mini_sentry, relay_chain): - relay = relay_chain() + relay = relay_chain(min_relay_version="latest") project_id = 42 mini_sentry.add_basic_project_config( From 6ebbd733a33e339b518917cb3b906a32dd92c7b2 Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Thu, 9 Jun 2022 11:02:23 -0700 Subject: [PATCH 23/24] black --- tests/integration/test_replay_events.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/test_replay_events.py b/tests/integration/test_replay_events.py index 2e02d82a992..384711d1f9d 100644 --- a/tests/integration/test_replay_events.py +++ b/tests/integration/test_replay_events.py @@ -55,4 +55,3 @@ def test_replay_events_without_processing(mini_sentry, relay_chain): replay_event = envelope.items[0] assert replay_event.type == "replay_event" - From 92b522da2310fbaff309ec0bd35def11512c64ae Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Thu, 9 Jun 2022 11:56:55 -0700 Subject: [PATCH 24/24] remove errant test file --- tests/integration/test_replay_payloads.py | 62 ----------------------- 1 file changed, 62 deletions(-) delete mode 100644 tests/integration/test_replay_payloads.py diff --git a/tests/integration/test_replay_payloads.py b/tests/integration/test_replay_payloads.py deleted file mode 100644 index 96bf73dffac..00000000000 --- a/tests/integration/test_replay_payloads.py +++ /dev/null @@ -1,62 +0,0 @@ -import pytest -import time -import uuid - -from requests.exceptions import HTTPError - - -def test_payload( - mini_sentry, relay_with_processing, replay_payloads_consumer, outcomes_consumer -): - project_id = 42 - event_id = "515539018c9b4260a6f999572f1661ee" - - relay = relay_with_processing() - mini_sentry.add_full_project_config( - project_id, extra={"config": {"features": ["organizations:session-replay"]}} - ) - replay_payloads_consumer = replay_payloads_consumer() - outcomes_consumer = outcomes_consumer() - - replay_payloads = [ - ("sentry_replay_payload", "sentry_replay_payload", b"test"), - ] - relay.send_attachments(project_id, event_id, replay_payloads) - - replay_payload_contents = {} - replay_payload_ids = [] - replay_payload_num_chunks = {} - - while set(replay_payload_contents.values()) != {b"test"}: - chunk, v = replay_payloads_consumer.get_replay_chunk() - replay_payload_contents[v["id"]] = ( - replay_payload_contents.get(v["id"], b"") + chunk - ) - if v["id"] not in replay_payload_ids: - replay_payload_ids.append(v["id"]) - num_chunks = 1 + replay_payload_num_chunks.get(v["id"], 0) - assert v["chunk_index"] == num_chunks - 1 - replay_payload_num_chunks[v["id"]] = num_chunks - - id1 = replay_payload_ids[0] - - assert replay_payload_contents[id1] == b"test" - - replay_payload = replay_payloads_consumer.get_individual_replay() - - assert replay_payload == { - "type": "replay_payload", - "attachment": { - "attachment_type": "event.attachment", - "chunks": replay_payload_num_chunks[id1], - "content_type": "application/octet-stream", - "id": id1, - "name": "sentry_replay_payload", - "size": len(replay_payload_contents[id1]), - "rate_limited": False, - }, - "event_id": event_id, - "project_id": project_id, - } - - outcomes_consumer.assert_empty()