Skip to main content

relay_event_schema/protocol/contexts/
mod.rs

1mod app;
2mod browser;
3mod chromium_stability_report;
4mod cloud_resource;
5mod culture;
6mod device;
7mod flags;
8mod gpu;
9mod memory_info;
10mod monitor;
11mod os;
12mod ota_updates;
13mod otel;
14mod performance_score;
15mod profile;
16mod replay;
17mod reprocessing;
18mod response;
19mod runtime;
20mod spring;
21mod threadpool_info;
22mod trace;
23mod unity;
24mod user_report_v2;
25pub use app::*;
26pub use browser::*;
27pub use chromium_stability_report::*;
28pub use cloud_resource::*;
29pub use culture::*;
30pub use device::*;
31pub use flags::*;
32pub use gpu::*;
33pub use memory_info::*;
34pub use monitor::*;
35pub use os::*;
36pub use ota_updates::*;
37pub use otel::*;
38pub use performance_score::*;
39pub use profile::*;
40pub use replay::*;
41pub use reprocessing::*;
42pub use response::*;
43pub use runtime::*;
44pub use spring::*;
45pub use threadpool_info::*;
46pub use trace::*;
47pub use unity::*;
48pub use user_report_v2::*;
49
50use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, Value};
51
52use crate::processor::ProcessValue;
53
54/// A span's operation type.
55///
56/// Tries to follow OpenCensus/OpenTracing's span types. Examples are `db.statement` for database
57/// queries or `http` for external HTTP calls.
58pub type OperationType = String;
59
60/// Origin type such as `auto.http`.
61/// Follows the pattern described in the [develop docs](https://develop.sentry.dev/sdk/performance/trace-origin/).
62pub type OriginType = String;
63
64/// A context describes environment info (e.g. device, os or browser).
65#[derive(Clone, Debug, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
66#[metastructure(process_func = "process_context")]
67pub enum Context {
68    /// Device information.
69    Device(#[metastructure(max_bytes = 8192)] Box<DeviceContext>),
70    /// Operating system information.
71    Os(#[metastructure(max_bytes = 8192)] Box<OsContext>),
72    /// Runtime information.
73    Runtime(#[metastructure(max_bytes = 8192)] Box<RuntimeContext>),
74    /// Application information.
75    App(#[metastructure(max_bytes = 8192)] Box<AppContext>),
76    /// Web browser information.
77    Browser(#[metastructure(max_bytes = 8192)] Box<BrowserContext>),
78    /// Information about device's GPU.
79    Gpu(#[metastructure(max_bytes = 8192)] Box<GpuContext>),
80    /// Information related to Tracing.
81    Trace(#[metastructure(max_bytes = 8192)] Box<TraceContext>),
82    /// Information related to Profiling.
83    Profile(#[metastructure(max_bytes = 8192)] Box<ProfileContext>),
84    /// Information related to Replay.
85    Replay(#[metastructure(max_bytes = 8192)] Box<ReplayContext>),
86    /// Information related to Feature flags.
87    Flags(#[metastructure(max_bytes = 65_536)] Box<flags::FlagsContext>),
88    /// Information related to User Report V2. TODO:(jferg): rename to UserFeedbackContext
89    #[metastructure(tag = "feedback")]
90    UserReportV2(#[metastructure(max_bytes = 8192)] Box<UserReportV2Context>),
91    /// Information related to Memory usage and garbage collection metrics.
92    #[metastructure(tag = "memory_info")]
93    MemoryInfo(#[metastructure(max_bytes = 8192)] Box<MemoryInfoContext>),
94    /// Information related to Monitors feature.
95    Monitor(#[metastructure(max_bytes = 8192)] Box<MonitorContext>),
96    /// Auxilliary information for reprocessing.
97    #[metastructure(omit_from_schema)]
98    Reprocessing(#[metastructure(max_bytes = 8192)] Box<ReprocessingContext>),
99    /// Response information.
100    Response(#[metastructure(max_bytes = 8192)] Box<ResponseContext>),
101    /// OpenTelemetry information.
102    Otel(#[metastructure(max_bytes = 8192)] Box<OtelContext>),
103    /// Cloud resource information.
104    CloudResource(#[metastructure(max_bytes = 8192)] Box<CloudResourceContext>),
105    /// Culture information.
106    Culture(#[metastructure(max_bytes = 8192)] Box<CultureContext>),
107    /// Performance score information.
108    PerformanceScore(#[metastructure(max_bytes = 8192)] Box<PerformanceScoreContext>),
109    /// Spring / Spring Boot information.
110    Spring(#[metastructure(max_bytes = 8192)] Box<SpringContext>),
111    /// Thread pool information.
112    #[metastructure(tag = "threadpool_info")]
113    ThreadPoolInfo(#[metastructure(max_bytes = 8192)] Box<ThreadPoolInfoContext>),
114    /// OTA Updates information.
115    OTAUpdates(#[metastructure(max_bytes = 8192)] Box<OTAUpdatesContext>),
116    /// Chromium Stability Report from minidump.
117    ChromiumStabilityReport(#[metastructure(max_bytes = 8192)] Box<StabilityReportContext>),
118    /// Unity information.
119    Unity(#[metastructure(max_bytes = 8192)] Box<UnityContext>),
120    /// Additional arbitrary fields for forwards compatibility.
121    #[metastructure(fallback_variant, retain = true)]
122    Other(#[metastructure(pii = "true", max_bytes = 8192)] Object<Value>),
123}
124
125#[derive(Clone, Debug, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
126pub struct ContextInner(#[metastructure(max_depth = 7)] pub Context);
127
128impl From<Context> for ContextInner {
129    fn from(c: Context) -> ContextInner {
130        ContextInner(c)
131    }
132}
133
134/// The Contexts interface provides additional context data. Typically, this is data related to the
135/// current user and the environment. For example, the device or application version. Its canonical
136/// name is `contexts`.
137///
138/// The `contexts` type can be used to define arbitrary contextual data on the event. It accepts an
139/// object of key/value pairs. The key is the “alias” of the context and can be freely chosen.
140/// However, as per policy, it should match the type of the context unless there are two values for
141/// a type. You can omit `type` if the key name is the type.
142///
143/// Unknown data for the contexts is rendered as a key/value list.
144///
145/// For more details about sending additional data with your event, see the [full documentation on
146/// Additional Data](https://docs.sentry.io/enriching-error-data/additional-data/).
147#[derive(Clone, Debug, PartialEq, Empty, IntoValue, ProcessValue, Default)]
148#[metastructure(process_func = "process_contexts")]
149pub struct Contexts(pub Object<ContextInner>);
150
151impl Contexts {
152    /// Creates an empty contexts map.
153    pub fn new() -> Contexts {
154        Contexts(Object::new())
155    }
156
157    /// Inserts a context under the default key for the context.
158    pub fn add<C>(&mut self, context: C)
159    where
160        C: DefaultContext,
161    {
162        self.insert(C::default_key().to_owned(), context.into_context());
163    }
164
165    /// Inserts a context under a custom given key.
166    ///
167    /// By convention, every typed context has a default key. Use [`add`](Self::add) to insert such
168    /// contexts, instead.
169    pub fn insert(&mut self, key: String, context: Context) {
170        self.0.insert(key, Annotated::new(ContextInner(context)));
171    }
172
173    /// Returns `true` if a matching context resides in the map at its default key.
174    pub fn contains<C>(&self) -> bool
175    where
176        C: DefaultContext,
177    {
178        // Use `get` to perform a type check.
179        self.get::<C>().is_some()
180    }
181
182    /// Returns `true` if a context with the provided key is present in the map.
183    ///
184    /// By convention, every typed context has a default key. Use [`contains`](Self::contains) to
185    /// check such contexts, instead.
186    pub fn contains_key<S>(&self, key: S) -> bool
187    where
188        S: AsRef<str>,
189    {
190        self.0.contains_key(key.as_ref())
191    }
192
193    /// Returns the context at its default key or constructs it if not present.
194    pub fn get_or_default<C>(&mut self) -> &mut C
195    where
196        C: DefaultContext,
197    {
198        if !self.contains::<C>() {
199            self.add(C::default());
200        }
201
202        self.get_mut().unwrap()
203    }
204
205    /// Returns the context at the specified key or constructs it if not present.
206    ///
207    /// By convention, every typed context has a default key. Use
208    /// [`get_or_default`](Self::get_or_default) to insert such contexts, instead.
209    pub fn get_or_insert_with<F, S>(&mut self, key: S, context_builder: F) -> &mut Context
210    where
211        F: FnOnce() -> Context,
212        S: Into<String>,
213    {
214        &mut self
215            .0
216            .entry(key.into())
217            .or_insert_with(Annotated::empty)
218            .value_mut()
219            .get_or_insert_with(|| ContextInner(context_builder()))
220            .0
221    }
222
223    /// Returns a reference to the default context by type.
224    pub fn get<C>(&self) -> Option<&C>
225    where
226        C: DefaultContext,
227    {
228        C::cast(self.get_key(C::default_key())?)
229    }
230
231    /// Returns a mutable reference to the default context by type.
232    pub fn get_mut<C>(&mut self) -> Option<&mut C>
233    where
234        C: DefaultContext,
235    {
236        C::cast_mut(self.get_key_mut(C::default_key())?)
237    }
238
239    /// Returns a reference to the context specified by `key`.
240    ///
241    /// By convention, every typed context has a default key. Use [`get`](Self::get) to retrieve
242    /// such contexts, instead.
243    pub fn get_key<S>(&self, key: S) -> Option<&Context>
244    where
245        S: AsRef<str>,
246    {
247        Some(&self.0.get(key.as_ref())?.value().as_ref()?.0)
248    }
249
250    /// Returns a mutable reference to the context specified by `key`.
251    ///
252    /// By convention, every typed context has a default key. Use [`get_mut`](Self::get_mut) to
253    /// retrieve such contexts, instead.
254    pub fn get_key_mut<S>(&mut self, key: S) -> Option<&mut Context>
255    where
256        S: AsRef<str>,
257    {
258        Some(&mut self.0.get_mut(key.as_ref())?.value_mut().as_mut()?.0)
259    }
260
261    /// Removes a context from the map, returning the context it was previously in the map.
262    ///
263    /// Returns `Some` if a matching context was removed from the default key. If the context at the
264    /// default key does not have a matching type, it is removed but `None` is returned.
265    pub fn remove<C>(&mut self) -> Option<C>
266    where
267        C: DefaultContext,
268    {
269        let context = self.remove_key(C::default_key())?;
270        C::from_context(context)
271    }
272
273    /// Removes a context from the map, returning the context it was previously in the map.
274    ///
275    /// By convention, every typed context has a default key. Use [`remove`](Self::remove) to
276    /// retrieve such contexts, instead.
277    pub fn remove_key<S>(&mut self, key: S) -> Option<Context>
278    where
279        S: AsRef<str>,
280    {
281        let inner = self.0.remove(key.as_ref())?;
282        Some(inner.into_value()?.0)
283    }
284}
285
286impl FromValue for Contexts {
287    fn from_value(mut annotated: Annotated<Value>) -> Annotated<Self> {
288        if let Annotated(Some(Value::Object(ref mut items)), _) = annotated {
289            for (key, value) in items.iter_mut() {
290                if let Annotated(Some(Value::Object(items)), _) = value {
291                    // Set the `"type"` if it's empty and overwrite it if it's an empty object or array.
292                    if !is_valid_context_type(items.get("type")) {
293                        items.insert(
294                            "type".to_owned(),
295                            Annotated::new(Value::String(key.to_string())),
296                        );
297                    }
298                }
299            }
300        }
301        FromValue::from_value(annotated).map_value(Contexts)
302    }
303}
304
305/// Returns `true` if `value` is a non-empty string, which is the only valid value
306/// for the `"type"` field of a context.
307fn is_valid_context_type(value: Option<&Annotated<Value>>) -> bool {
308    matches!(value.and_then(|v| v.value()), Some(Value::String(s)) if !s.is_empty())
309}
310
311/// A well-known context in the [`Contexts`] interface.
312///
313/// These contexts have a [default key](Self::default_key) in the contexts map and can be
314/// constructed as an empty default value.
315pub trait DefaultContext: Default {
316    /// The default key at which this context resides in [`Contexts`].
317    fn default_key() -> &'static str;
318
319    /// Converts this context type from a generic context type.
320    ///
321    /// Returns `Some` if the context is of this type. Otherwise, returns `None`.
322    fn from_context(context: Context) -> Option<Self>;
323
324    /// Casts a reference to this context type from a generic context type.
325    ///
326    /// Returns `Some` if the context is of this type. Otherwise, returns `None`.
327    fn cast(context: &Context) -> Option<&Self>;
328
329    /// Casts a mutable reference to this context type from a generic context type.
330    ///
331    /// Returns `Some` if the context is of this type. Otherwise, returns `None`.
332    fn cast_mut(context: &mut Context) -> Option<&mut Self>;
333
334    /// Boxes this context type in the generic context wrapper.
335    ///
336    /// Returns `Some` if the context is of this type. Otherwise, returns `None`.
337    fn into_context(self) -> Context;
338}
339
340#[cfg(test)]
341mod tests {
342    use relay_protocol::{Map, Meta};
343
344    use super::*;
345    use crate::processor::{ProcessingResult, ProcessingState, Processor};
346    use crate::protocol::Event;
347
348    #[test]
349    fn test_other_context_roundtrip() {
350        let json = r#"{"other":"value","type":"mytype"}"#;
351        let context = Annotated::new(Context::Other({
352            let mut map = Map::new();
353            map.insert(
354                "other".to_owned(),
355                Annotated::new(Value::String("value".to_owned())),
356            );
357            map.insert(
358                "type".to_owned(),
359                Annotated::new(Value::String("mytype".to_owned())),
360            );
361            map
362        }));
363
364        assert_eq!(context, Annotated::from_json(json).unwrap());
365        assert_eq!(json, context.to_json().unwrap());
366    }
367
368    #[test]
369    fn test_untagged_context_deserialize() {
370        let json = r#"{"os": {"name": "Linux"}}"#;
371
372        let mut map = Contexts::new();
373        map.add(OsContext {
374            name: Annotated::new("Linux".to_owned()),
375            ..Default::default()
376        });
377
378        assert_eq!(Annotated::new(map), Annotated::from_json(json).unwrap());
379    }
380
381    #[test]
382    fn test_context_invalid_type_deserialize() {
383        let json = r#"{
384            "monitor":{"name":"Foobar","type":17},
385            "os":{"name":"Linux","type":{}},
386            "profile":{"profile_id":"52df9022835246eeb317dbd739ccd059","type":""},
387            "runtime":{"name":"rustc","type":["invalid"]}
388        }"#;
389
390        let mut map = Contexts::new();
391        map.add(MonitorContext(
392            [("name".to_owned(), Value::String("Foobar".to_owned()).into())]
393                .into_iter()
394                .collect(),
395        ));
396        map.add(OsContext {
397            name: Annotated::new("Linux".to_owned()),
398            ..Default::default()
399        });
400        map.add(ProfileContext {
401            profile_id: Annotated::new("52df9022835246eeb317dbd739ccd059".parse().unwrap()),
402            ..Default::default()
403        });
404        map.add(RuntimeContext {
405            name: Annotated::new("rustc".to_owned()),
406            ..Default::default()
407        });
408
409        assert_eq!(Annotated::new(map), Annotated::from_json(json).unwrap());
410    }
411
412    #[test]
413    fn test_multiple_contexts_roundtrip() {
414        let json =
415            r#"{"os":{"name":"Linux","type":"os"},"runtime":{"name":"rustc","type":"runtime"}}"#;
416
417        let mut map = Contexts::new();
418        map.add(OsContext {
419            name: Annotated::new("Linux".to_owned()),
420            ..Default::default()
421        });
422        map.add(RuntimeContext {
423            name: Annotated::new("rustc".to_owned()),
424            ..Default::default()
425        });
426
427        let contexts = Annotated::new(map);
428        assert_eq!(contexts, Annotated::from_json(json).unwrap());
429        assert_eq!(json, contexts.to_json().unwrap());
430    }
431
432    #[test]
433    fn test_context_processing() {
434        let mut event = Annotated::new(Event {
435            contexts: {
436                let mut contexts = Contexts::new();
437                contexts.add(RuntimeContext {
438                    name: Annotated::new("php".to_owned()),
439                    version: Annotated::new("7.1.20-1+ubuntu16.04.1+deb.sury.org+1".to_owned()),
440                    ..Default::default()
441                });
442                Annotated::new(contexts)
443            },
444            ..Default::default()
445        });
446
447        struct FooProcessor {
448            called: bool,
449        }
450
451        impl Processor for FooProcessor {
452            #[inline]
453            fn process_context(
454                &mut self,
455                _value: &mut Context,
456                _meta: &mut Meta,
457                _state: &ProcessingState<'_>,
458            ) -> ProcessingResult {
459                self.called = true;
460                Ok(())
461            }
462        }
463
464        let mut processor = FooProcessor { called: false };
465        crate::processor::process_value(&mut event, &mut processor, ProcessingState::root())
466            .unwrap();
467        assert!(processor.called);
468    }
469}