diff --git a/CHANGELOG.md b/CHANGELOG.md index 89f0c8ca07c..9311cb4ebe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Metrics: **Features**: - Allow monitor checkins to paass `monitor_config` for monitor upserts. ([#1962](https://github.com/getsentry/relay/pull/1962)) +- Add replay_id onto event from dynamic sampling context. ([#1983](https://github.com/getsentry/relay/pull/1983)) - Add product-name for devices, derived from the android model. ([#2004](https://github.com/getsentry/relay/pull/2004)) - Changes how device class is determined for iPhone devices. Instead of checking processor frequency, the device model is mapped to a device class. ([#1970](https://github.com/getsentry/relay/pull/1970)) - Don't sanitize transactions if no clustering rules exist and no UUIDs were scrubbed. ([#1976](https://github.com/getsentry/relay/pull/1976)) diff --git a/relay-general/benches/benchmarks.rs b/relay-general/benches/benchmarks.rs index 092a9436632..34d59708d44 100644 --- a/relay-general/benches/benchmarks.rs +++ b/relay-general/benches/benchmarks.rs @@ -92,6 +92,7 @@ fn bench_store_processor(c: &mut Criterion) { breakdowns: None, span_attributes: Default::default(), client_sample_rate: None, + replay_id: None, client_hints: ClientHints::default(), }; diff --git a/relay-general/src/protocol/contexts/mod.rs b/relay-general/src/protocol/contexts/mod.rs index 9c80d5dca20..7502214436a 100644 --- a/relay-general/src/protocol/contexts/mod.rs +++ b/relay-general/src/protocol/contexts/mod.rs @@ -12,6 +12,8 @@ mod os; pub use os::*; mod profile; pub use profile::*; +mod replay; +pub use replay::*; mod reprocessing; pub use reprocessing::*; mod response; @@ -57,6 +59,8 @@ pub enum Context { Trace(Box), /// Information related to Profiling. Profile(Box), + /// Information related to Replay. + Replay(Box), /// Information related to Monitors feature. Monitor(Box), /// Auxilliary information for reprocessing. @@ -89,6 +93,7 @@ impl Context { Context::Trace(_) => Some(TraceContext::default_key()), Context::Profile(_) => Some(ProfileContext::default_key()), Context::Monitor(_) => Some(MonitorContext::default_key()), + Context::Replay(_) => Some(ReplayContext::default_key()), Context::Response(_) => Some(ResponseContext::default_key()), Context::Otel(_) => Some(OtelContext::default_key()), Context::CloudResource(_) => Some(CloudResourceContext::default_key()), diff --git a/relay-general/src/protocol/contexts/replay.rs b/relay-general/src/protocol/contexts/replay.rs new file mode 100644 index 00000000000..bfe92036c60 --- /dev/null +++ b/relay-general/src/protocol/contexts/replay.rs @@ -0,0 +1,61 @@ +use crate::protocol::EventId; +use crate::types::{Annotated, Object, Value}; + +/// Replay context. +/// +/// The replay context contains the replay_id of the session replay if the event +/// occurred during a replay. The replay_id is added onto the dynamic sampling context +/// on the javascript SDK which propagates it through the trace. In relay, we take +/// this value from the DSC and create a context which contains only the replay_id +/// This context is never set on the client for events, only on relay. +#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] +pub struct ReplayContext { + /// The replay ID. + pub replay_id: Annotated, + /// Additional arbitrary fields for forwards compatibility. + #[metastructure(additional_properties, retain = "true")] + pub other: Object, +} + +impl ReplayContext { + /// The key under which a replay context is generally stored (in `Contexts`). + pub fn default_key() -> &'static str { + "replay" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::protocol::Context; + + #[test] + pub(crate) fn test_trace_context_roundtrip() { + let json = r#"{ + "replay_id": "4c79f60c11214eb38604f4ae0781bfb2", + "type": "replay" +}"#; + let context = Annotated::new(Context::Replay(Box::new(ReplayContext { + replay_id: Annotated::new(EventId("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap())), + other: Object::default(), + }))); + + assert_eq!(context, Annotated::from_json(json).unwrap()); + assert_eq!(json, context.to_json_pretty().unwrap()); + } + + #[test] + pub(crate) fn test_replay_context_normalization() { + let json = r#"{ + "replay_id": "4C79F60C11214EB38604F4AE0781BFB2", + "type": "replay" +}"#; + let context = Annotated::new(Context::Replay(Box::new(ReplayContext { + replay_id: Annotated::new(EventId("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap())), + other: Object::default(), + }))); + + assert_eq!(context, Annotated::from_json(json).unwrap()); + } +} diff --git a/relay-general/src/store/mod.rs b/relay-general/src/store/mod.rs index be9ddd4a087..7182827e8a7 100644 --- a/relay-general/src/store/mod.rs +++ b/relay-general/src/store/mod.rs @@ -3,6 +3,7 @@ use std::collections::BTreeSet; use std::sync::Arc; use chrono::{DateTime, Utc}; +use relay_common::Uuid; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -66,6 +67,8 @@ pub struct StoreConfig { /// The SDK's sample rate as communicated via envelope headers. pub client_sample_rate: Option, + /// The replay_id associated with the current event communicated via envelope headers. + pub replay_id: Option, } /// The processor that normalizes events for store. diff --git a/relay-general/src/store/normalize.rs b/relay-general/src/store/normalize.rs index 5326be21aba..baba2911b6f 100644 --- a/relay-general/src/store/normalize.rs +++ b/relay-general/src/store/normalize.rs @@ -16,7 +16,8 @@ use crate::processor::{MaxChars, ProcessValue, ProcessingState, Processor}; use crate::protocol::{ self, AsPair, Breadcrumb, ClientSdkInfo, Context, Contexts, DebugImage, DeviceClass, Event, EventId, EventType, Exception, Frame, Headers, IpAddr, Level, LogEntry, Measurement, - Measurements, Request, SpanStatus, Stacktrace, Tags, TraceContext, User, VALID_PLATFORMS, + Measurements, ReplayContext, Request, SpanStatus, Stacktrace, Tags, TraceContext, User, + VALID_PLATFORMS, }; use crate::store::{ClockDriftProcessor, GeoIpLookup, StoreConfig, TransactionNameConfig}; use crate::types::{ @@ -199,6 +200,16 @@ impl<'a> NormalizeProcessor<'a> { } } } + fn normalize_replay_context(&self, event: &mut Event) { + if let Some(ref mut contexts) = event.contexts.value_mut() { + if let Some(replay_id) = self.config.replay_id { + contexts.add(Context::Replay(Box::new(ReplayContext { + replay_id: Annotated::new(EventId(replay_id)), + other: Object::default(), + }))); + } + } + } /// Infers the `EventType` from the event's interfaces. fn infer_event_type(&self, event: &Event) -> EventType { @@ -821,6 +832,7 @@ impl<'a> Processor for NormalizeProcessor<'a> { // Normalize connected attributes and interfaces self.normalize_spans(event); self.normalize_trace_context(event); + self.normalize_replay_context(event); Ok(()) } @@ -1123,6 +1135,7 @@ fn remove_logger_word(tokens: &mut Vec<&str>) { mod tests { use chrono::TimeZone; use insta::assert_debug_snapshot; + use relay_common::Uuid; use serde_json::json; use similar_asserts::assert_eq; @@ -1471,6 +1484,39 @@ mod tests { process_value(&mut event, &mut processor, ProcessingState::root()).unwrap(); assert_eq!(get_value!(event.environment), None); } + #[test] + fn test_replay_id_added_from_dsc() { + let replay_id = Uuid::new_v4(); + let mut event = Annotated::new(Event { + contexts: Annotated::new(Contexts(Object::new())), + ..Event::default() + }); + let config = StoreConfig { + replay_id: Some(replay_id), + ..StoreConfig::default() + }; + let mut processor = NormalizeProcessor::new(Arc::new(config), None); + let config = LightNormalizationConfig::default(); + light_normalize_event(&mut event, config).unwrap(); + process_value(&mut event, &mut processor, ProcessingState::root()).unwrap(); + + let event = event.value().unwrap(); + + assert_eq!( + event.contexts, + Annotated::new(Contexts({ + let mut contexts = Object::new(); + contexts.insert( + "replay".to_owned(), + Annotated::new(ContextInner(Context::Replay(Box::new(ReplayContext { + replay_id: Annotated::new(EventId(replay_id)), + other: Object::default(), + })))), + ); + contexts + })) + ) + } #[test] fn test_none_environment_errors() { diff --git a/relay-general/tests/snapshots/test_fixtures__event_schema.snap b/relay-general/tests/snapshots/test_fixtures__event_schema.snap index f394690d0f0..b49ec1e4911 100644 --- a/relay-general/tests/snapshots/test_fixtures__event_schema.snap +++ b/relay-general/tests/snapshots/test_fixtures__event_schema.snap @@ -896,6 +896,9 @@ expression: "relay_general::protocol::event_json_schema()" { "$ref": "#/definitions/ProfileContext" }, + { + "$ref": "#/definitions/ReplayContext" + }, { "$ref": "#/definitions/MonitorContext" }, @@ -2736,6 +2739,29 @@ expression: "relay_general::protocol::event_json_schema()" "RegVal": { "type": "string" }, + "ReplayContext": { + "description": " Replay context.\n\n The replay context contains the replay_id of the session replay if the event\n occurred during a replay. The replay_id is added onto the dynamic sampling context\n on the javascript SDK which propagates it through the trace. In relay, we take\n this value from the DSC and create a context which contains only the replay_id\n This context is never set on the client for events, only on relay.", + "anyOf": [ + { + "type": "object", + "properties": { + "replay_id": { + "description": " The replay ID.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/EventId" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + ] + }, "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": [ diff --git a/relay-sampling/src/lib.rs b/relay-sampling/src/lib.rs index 510b9f2402b..36a8d5a7db3 100644 --- a/relay-sampling/src/lib.rs +++ b/relay-sampling/src/lib.rs @@ -1164,6 +1164,8 @@ pub struct DynamicSamplingContext { /// user object). #[serde(flatten, default)] pub user: TraceUserContext, + /// If the event occurred during a session replay, the associated replay_id is added to the DSC. + pub replay_id: Option, /// Additional arbitrary fields for forwards compatibility. #[serde(flatten, default)] pub other: BTreeMap, @@ -1197,6 +1199,7 @@ impl DynamicSamplingContext { release: event.release.as_str().map(str::to_owned), environment: event.environment.value().cloned(), transaction: event.transaction.value().cloned(), + replay_id: None, sample_rate: None, user: TraceUserContext { user_segment: user @@ -1311,6 +1314,7 @@ mod tests { transaction: None, sample_rate: None, user: TraceUserContext::default(), + replay_id: None, other: BTreeMap::new(), } } @@ -1413,6 +1417,7 @@ mod tests { user_segment: user_segment.to_string(), user_id: user_id.to_string(), }, + replay_id: Default::default(), other: Default::default(), } } @@ -1587,6 +1592,7 @@ mod tests { environment: Some("prod".into()), transaction: Some("transaction1".into()), sample_rate: None, + replay_id: Some(Uuid::new_v4()), other: BTreeMap::new(), }; @@ -1622,6 +1628,7 @@ mod tests { environment: None, transaction: None, sample_rate: None, + replay_id: None, other: BTreeMap::new(), }; assert_eq!(Value::Null, dsc.get_value("trace.release")); @@ -1638,6 +1645,7 @@ mod tests { environment: None, transaction: None, sample_rate: None, + replay_id: None, other: BTreeMap::new(), }; assert_eq!(Value::Null, dsc.get_value("trace.user.id")); @@ -1742,6 +1750,7 @@ mod tests { user_segment: "vip".into(), user_id: "user-id".into(), }, + replay_id: Some(Uuid::new_v4()), environment: Some("debug".into()), transaction: Some("transaction1".into()), sample_rate: None, @@ -1917,6 +1926,7 @@ mod tests { user_segment: "vip".to_owned(), user_id: "user-id".to_owned(), }, + replay_id: Some(Uuid::new_v4()), environment: Some("debug".to_string()), transaction: Some("transaction1".into()), sample_rate: None, @@ -1979,6 +1989,7 @@ mod tests { user_segment: "vip".to_owned(), user_id: "user-id".to_owned(), }, + replay_id: Some(Uuid::new_v4()), environment: Some("debug".to_string()), transaction: Some("transaction1".into()), sample_rate: None, @@ -2018,6 +2029,7 @@ mod tests { user_segment: "vip".to_owned(), user_id: "user-id".to_owned(), }, + replay_id: Some(Uuid::new_v4()), environment: Some("debug".to_string()), transaction: Some("transaction1".into()), sample_rate: None, @@ -2080,6 +2092,7 @@ mod tests { user_segment: "vip".to_owned(), user_id: "user-id".to_owned(), }, + replay_id: Some(Uuid::new_v4()), environment: Some("debug".to_string()), transaction: Some("transaction1".into()), sample_rate: None, @@ -2467,6 +2480,7 @@ mod tests { user_segment: "vip".to_owned(), user_id: "user-id".to_owned(), }, + replay_id: Some(Uuid::new_v4()), environment: Some("debug".to_string()), transaction: Some("transaction1".into()), sample_rate: None, @@ -2487,6 +2501,7 @@ mod tests { public_key: ProjectKey::parse("abd0f232775f45feab79864e580d160b").unwrap(), release: Some("1.1.1".to_string()), user: TraceUserContext::default(), + replay_id: Some(Uuid::new_v4()), environment: Some("debug".to_string()), transaction: Some("transaction1".into()), sample_rate: None, @@ -2510,6 +2525,7 @@ mod tests { user_segment: "vip".to_owned(), user_id: "user-id".to_owned(), }, + replay_id: None, environment: None, transaction: Some("transaction1".into()), sample_rate: None, @@ -2533,6 +2549,7 @@ mod tests { user_segment: "vip".to_owned(), user_id: "user-id".to_owned(), }, + replay_id: None, environment: Some("debug".to_string()), transaction: None, sample_rate: None, @@ -2549,6 +2566,7 @@ mod tests { public_key: ProjectKey::parse("abd0f232775f45feab79864e580d160b").unwrap(), release: None, user: TraceUserContext::default(), + replay_id: None, environment: None, transaction: None, sample_rate: None, @@ -2944,6 +2962,7 @@ mod tests { "environment": None, "transaction": None, "user_id": "hello", + "replay_id": None, } "###); } @@ -2968,6 +2987,7 @@ mod tests { "transaction": None, "sample_rate": "0.5", "user_id": "hello", + "replay_id": None, } "###); } @@ -2992,6 +3012,7 @@ mod tests { "transaction": None, "sample_rate": "0.00001", "user_id": "hello", + "replay_id": None, } "###); } diff --git a/relay-server/src/actors/processor.rs b/relay-server/src/actors/processor.rs index a5e3ab7b7e8..165afd09d37 100644 --- a/relay-server/src/actors/processor.rs +++ b/relay-server/src/actors/processor.rs @@ -1891,6 +1891,7 @@ impl EnvelopeProcessorService { breakdowns: project_state.config.breakdowns_v2.clone(), span_attributes: project_state.config.span_attributes.clone(), client_sample_rate: envelope.dsc().and_then(|ctx| ctx.sample_rate), + replay_id: envelope.dsc().and_then(|ctx| ctx.replay_id), client_hints: envelope.meta().client_hints().to_owned(), }; @@ -2583,6 +2584,7 @@ mod tests { use std::str::FromStr; use chrono::{DateTime, TimeZone, Utc}; + use relay_general::pii::{DataScrubbingConfig, PiiConfig}; use relay_general::protocol::{EventId, TransactionSource}; use relay_general::store::{LazyGlob, RedactionRule, RuleScope, TransactionNameRule}; diff --git a/relay-server/src/testutils.rs b/relay-server/src/testutils.rs index a1ae3214cea..f69f248d235 100644 --- a/relay-server/src/testutils.rs +++ b/relay-server/src/testutils.rs @@ -48,6 +48,7 @@ pub fn create_sampling_context(sample_rate: Option) -> DynamicSamplingConte transaction: None, sample_rate, user: Default::default(), + replay_id: None, other: Default::default(), } } diff --git a/relay-server/src/utils/dynamic_sampling.rs b/relay-server/src/utils/dynamic_sampling.rs index 6e79ba7c65d..21e6a13ec3e 100644 --- a/relay-server/src/utils/dynamic_sampling.rs +++ b/relay-server/src/utils/dynamic_sampling.rs @@ -226,6 +226,7 @@ mod tests { transaction: transaction.map(|value| value.to_string()), sample_rate, user: Default::default(), + replay_id: Default::default(), other: Default::default(), } }