relay_event_schema/protocol/
clientsdk.rs

1use crate::processor::ProcessValue;
2use crate::protocol::IpAddr;
3use relay_protocol::{
4    Annotated, Array, Empty, ErrorKind, FromValue, IntoValue, Object, SkipSerialization, Value,
5};
6use serde::{Serialize, Serializer};
7use std::str::FromStr;
8use thiserror::Error;
9
10/// An installed and loaded package as part of the Sentry SDK.
11#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
12pub struct ClientSdkPackage {
13    /// Name of the package.
14    pub name: Annotated<String>,
15    /// Version of the package.
16    pub version: Annotated<String>,
17}
18
19/// An error returned when parsing setting values fail.
20#[derive(Debug, Clone, Error)]
21pub enum ParseSettingError {
22    #[error("Invalid value for 'infer_ip'.")]
23    InferIp,
24}
25
26/// A collection of settings that are used to control behaviour in relay through flags.
27///
28/// The settings aim to replace magic values in fields which need special treatment,
29/// for example `{{auto}}` in the user.ip_address. The SDK would instead send `infer_ip`
30/// to toggle the behaviour.
31#[derive(Debug, Clone, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
32pub struct ClientSdkSettings {
33    infer_ip: Annotated<AutoInferSetting>,
34}
35
36impl ClientSdkSettings {
37    /// Returns the current [`AutoInferSetting`] setting.
38    ///
39    /// **NOTE**: For forwards compatibility, this method has two defaults:
40    /// * If `settings.infer_ip` is missing entirely, it will default
41    ///   to [`AutoInferSetting::Legacy`].
42    /// * If `settings.infer_ip` contains an invalid value, it will default
43    ///   to [`AutoInferSetting::Never`].
44    ///
45    /// The reason behind this is that we don't want to fall back to the legacy behaviour
46    /// if we add a new value to [`AutoInferSetting`] and a relay is running an old version
47    /// which does not have the new value yet.
48    pub fn infer_ip(&self) -> AutoInferSetting {
49        if self.infer_ip.meta().has_errors() {
50            AutoInferSetting::Never
51        } else {
52            self.infer_ip.value().copied().unwrap_or_default()
53        }
54    }
55}
56
57/// Used to control the IP inference setting in relay. This is used as an alternative to magic
58/// values like {{auto}} in the user.ip_address field.
59#[derive(Debug, Copy, Clone, PartialEq, Default, ProcessValue)]
60pub enum AutoInferSetting {
61    /// Derive the IP address from the connection information.
62    Auto,
63
64    /// Do not derive the IP address, keep what was being sent by the client.
65    Never,
66
67    /// Enables the legacy IP inference behaviour.
68    ///
69    /// The legacy behavior works mainly by inspecting the content of `user.ip_address` and
70    /// decides based on the value.
71    /// Unfortunately, not all platforms are treated equals so there are exceptions for
72    /// `javascript`, `cocoa` and `objc`.
73    ///
74    /// If the value in `ip_address` is `{{auto}}`, it will work the
75    /// same as [`AutoInferSetting::Auto`]. This is true for all platforms.
76    ///
77    /// If the value in `ip_address` is [`None`], it will only infer the IP address if a
78    /// `REMOTE_ADDR` header is sent in the request payload of the event.
79    ///
80    /// **NOTE**: Setting `ip_address` to [`None`] will behave the same as setting it to `{{auto}}`
81    ///           for `javascript`, `cocoa` and `objc`.
82    #[default]
83    Legacy,
84}
85
86impl Empty for AutoInferSetting {
87    fn is_empty(&self) -> bool {
88        matches!(self, AutoInferSetting::Legacy)
89    }
90}
91
92impl AutoInferSetting {
93    /// Returns a string representation for [`AutoInferSetting`].
94    pub fn as_str(&self) -> &'static str {
95        match self {
96            AutoInferSetting::Auto => "auto",
97            AutoInferSetting::Never => "never",
98            AutoInferSetting::Legacy => "legacy",
99        }
100    }
101}
102
103impl FromStr for AutoInferSetting {
104    type Err = ParseSettingError;
105
106    fn from_str(s: &str) -> Result<Self, Self::Err> {
107        match s {
108            "auto" => Ok(AutoInferSetting::Auto),
109            "never" => Ok(AutoInferSetting::Never),
110            "legacy" => Ok(AutoInferSetting::Legacy),
111            _ => Err(ParseSettingError::InferIp),
112        }
113    }
114}
115
116impl FromValue for AutoInferSetting {
117    fn from_value(value: Annotated<Value>) -> Annotated<Self>
118    where
119        Self: Sized,
120    {
121        match String::from_value(value) {
122            Annotated(Some(value), mut meta) => match value.parse() {
123                Ok(infer_ip) => Annotated(Some(infer_ip), meta),
124                Err(_) => {
125                    meta.add_error(ErrorKind::InvalidData);
126                    meta.set_original_value(Some(value));
127                    Annotated(None, meta)
128                }
129            },
130            Annotated(None, meta) => Annotated(None, meta),
131        }
132    }
133}
134
135impl IntoValue for AutoInferSetting {
136    fn into_value(self) -> Value {
137        Value::String(self.as_str().to_string())
138    }
139
140    fn serialize_payload<S>(&self, s: S, _: SkipSerialization) -> Result<S::Ok, S::Error>
141    where
142        Self: Sized,
143        S: Serializer,
144    {
145        Serialize::serialize(self.as_str(), s)
146    }
147}
148
149/// The SDK Interface describes the Sentry SDK and its configuration used to capture and transmit an event.
150#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
151#[metastructure(process_func = "process_client_sdk_info", value_type = "ClientSdkInfo")]
152pub struct ClientSdkInfo {
153    /// Unique SDK name. _Required._
154    ///
155    /// The name of the SDK. The format is `entity.ecosystem[.flavor]` where entity identifies the
156    /// developer of the SDK, ecosystem refers to the programming language or platform where the
157    /// SDK is to be used and the optional flavor is used to identify standalone SDKs that are part
158    /// of a major ecosystem.
159    ///
160    /// Official Sentry SDKs use the entity `sentry`, as in `sentry.python` or
161    /// `sentry.javascript.react-native`. Please use a different entity for your own SDKs.
162    #[metastructure(required = true, max_chars = 256, max_chars_allowance = 20)]
163    pub name: Annotated<String>,
164
165    /// The version of the SDK. _Required._
166    ///
167    /// It should have the [Semantic Versioning](https://semver.org/) format `MAJOR.MINOR.PATCH`,
168    /// without any prefix (no `v` or anything else in front of the major version number).
169    ///
170    /// Examples: `0.1.0`, `1.0.0`, `4.3.12`
171    #[metastructure(required = true, max_chars = 256, max_chars_allowance = 20)]
172    pub version: Annotated<String>,
173
174    /// List of integrations that are enabled in the SDK. _Optional._
175    ///
176    /// The list should have all enabled integrations, including default integrations. Default
177    /// integrations are included because different SDK releases may contain different default
178    /// integrations.
179    #[metastructure(skip_serialization = "empty_deep")]
180    pub integrations: Annotated<Array<String>>,
181
182    /// List of features that are enabled in the SDK. _Optional._
183    ///
184    /// A list of feature names identifying enabled SDK features. This list
185    /// should contain all enabled SDK features. On some SDKs, enabling a feature in the
186    /// options also adds an integration. We encourage tracking such features with either
187    /// integrations or features but not both to reduce the payload size.
188    #[metastructure(skip_serialization = "empty_deep")]
189    pub features: Annotated<Array<String>>,
190
191    /// List of installed and loaded SDK packages. _Optional._
192    ///
193    /// A list of packages that were installed as part of this SDK or the activated integrations.
194    /// Each package consists of a name in the format `source:identifier` and `version`. If the
195    /// source is a Git repository, the `source` should be `git`, the identifier should be a
196    /// checkout link and the version should be a Git reference (branch, tag or SHA).
197    #[metastructure(skip_serialization = "empty_deep")]
198    pub packages: Annotated<Array<ClientSdkPackage>>,
199
200    /// IP Address of sender??? Seems unused. Do not send, this only leads to surprises wrt PII, as
201    /// the value appears nowhere in the UI.
202    #[metastructure(pii = "true", skip_serialization = "empty", omit_from_schema)]
203    pub client_ip: Annotated<IpAddr>,
204
205    /// Settings that are used to control behaviour of relay.
206    #[metastructure(skip_serialization = "empty")]
207    pub settings: Annotated<ClientSdkSettings>,
208
209    /// Additional arbitrary fields for forwards compatibility.
210    #[metastructure(additional_properties)]
211    pub other: Object<Value>,
212}
213
214impl ClientSdkInfo {
215    pub fn has_integration<T: AsRef<str>>(&self, integration: T) -> bool {
216        if let Some(integrations) = self.integrations.value() {
217            for x in integrations {
218                if x.as_str().unwrap_or_default() == integration.as_ref() {
219                    return true;
220                }
221            }
222        };
223        false
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use relay_protocol::Map;
230    use similar_asserts::assert_eq;
231
232    use super::*;
233
234    #[test]
235    fn test_client_sdk_roundtrip() {
236        let json = r#"{
237  "name": "sentry.rust",
238  "version": "1.0.0",
239  "integrations": [
240    "actix"
241  ],
242  "features": [
243    "feature1"
244  ],
245  "packages": [
246    {
247      "name": "cargo:sentry",
248      "version": "0.10.0"
249    },
250    {
251      "name": "cargo:sentry-actix",
252      "version": "0.10.0"
253    }
254  ],
255  "client_ip": "127.0.0.1",
256  "other": "value"
257}"#;
258        let sdk = Annotated::new(ClientSdkInfo {
259            name: Annotated::new("sentry.rust".to_string()),
260            version: Annotated::new("1.0.0".to_string()),
261            integrations: Annotated::new(vec![Annotated::new("actix".to_string())]),
262            features: Annotated::new(vec![Annotated::new("feature1".to_string())]),
263            packages: Annotated::new(vec![
264                Annotated::new(ClientSdkPackage {
265                    name: Annotated::new("cargo:sentry".to_string()),
266                    version: Annotated::new("0.10.0".to_string()),
267                }),
268                Annotated::new(ClientSdkPackage {
269                    name: Annotated::new("cargo:sentry-actix".to_string()),
270                    version: Annotated::new("0.10.0".to_string()),
271                }),
272            ]),
273            client_ip: Annotated::new(IpAddr("127.0.0.1".to_owned())),
274            settings: Annotated::empty(),
275            other: {
276                let mut map = Map::new();
277                map.insert(
278                    "other".to_string(),
279                    Annotated::new(Value::String("value".to_string())),
280                );
281                map
282            },
283        });
284
285        assert_eq!(sdk, Annotated::from_json(json).unwrap());
286        assert_eq!(json, sdk.to_json_pretty().unwrap());
287    }
288
289    #[test]
290    fn test_client_sdk_default_values() {
291        let json = r#"{
292  "name": "sentry.rust",
293  "version": "1.0.0",
294  "client_ip": "127.0.0.1"
295}"#;
296        let sdk = Annotated::new(ClientSdkInfo {
297            name: Annotated::new("sentry.rust".to_string()),
298            version: Annotated::new("1.0.0".to_string()),
299            integrations: Annotated::empty(),
300            features: Annotated::empty(),
301            packages: Annotated::empty(),
302            client_ip: Annotated::new(IpAddr("127.0.0.1".to_owned())),
303            settings: Annotated::empty(),
304            other: Default::default(),
305        });
306
307        assert_eq!(sdk, Annotated::from_json(json).unwrap());
308        assert_eq!(json, sdk.to_json_pretty().unwrap());
309    }
310
311    #[test]
312    fn test_sdk_settings_auto() {
313        let json = r#"{
314  "settings": {
315    "infer_ip": "auto"
316  }
317}"#;
318        let sdk = Annotated::new(ClientSdkInfo {
319            settings: Annotated::new(ClientSdkSettings {
320                infer_ip: Annotated::new(AutoInferSetting::Auto),
321            }),
322            ..Default::default()
323        });
324
325        assert_eq!(sdk, Annotated::from_json(json).unwrap());
326        assert_eq!(json, sdk.to_json_pretty().unwrap());
327    }
328
329    #[test]
330    fn test_sdk_settings_never() {
331        let json = r#"{
332  "settings": {
333    "infer_ip": "never"
334  }
335}"#;
336        let sdk = Annotated::new(ClientSdkInfo {
337            settings: Annotated::new(ClientSdkSettings {
338                infer_ip: Annotated::new(AutoInferSetting::Never),
339            }),
340            ..Default::default()
341        });
342
343        assert_eq!(sdk, Annotated::from_json(json).unwrap());
344        assert_eq!(json, sdk.to_json_pretty().unwrap());
345    }
346
347    #[test]
348    fn test_sdk_settings_default() {
349        let sdk = Annotated::new(ClientSdkInfo {
350            settings: Annotated::new(ClientSdkSettings {
351                infer_ip: Annotated::empty(),
352            }),
353            ..Default::default()
354        });
355
356        assert_eq!(
357            sdk.value().unwrap().settings.value().unwrap().infer_ip(),
358            AutoInferSetting::Legacy
359        )
360    }
361
362    #[test]
363    fn test_infer_ip_invalid() {
364        let json = r#"{
365            "settings": {
366                "infer_ip": "invalid"
367            }
368        }"#;
369        let sdk: Annotated<ClientSdkInfo> = Annotated::from_json(json).unwrap();
370        assert_eq!(
371            sdk.value().unwrap().settings.value().unwrap().infer_ip(),
372            AutoInferSetting::Never
373        );
374    }
375}