relay_conventions/
lib.rs

1//! Attribute definitions extracted from [`sentry-conventions`](https://github.com/getsentry/sentry-conventions).
2//!
3//! This crate contains the `sentry-conventions` repository as a git submodule. Attribute definitions in the submodule
4//! are parsed at compile time and can be accessed via the `attribute_info` function.
5//!
6//! It also exposes a number of constants for attribute names that Relay has specific logic for. It is recommended
7//! to use these constants instead of the bare attribute names to ensure consistency.
8
9pub mod consts;
10
11pub use consts::*;
12
13include!(concat!(env!("OUT_DIR"), "/attribute_map.rs"));
14include!(concat!(env!("OUT_DIR"), "/name_fn.rs"));
15
16/// Whether an attribute should be PII-strippable/should be subject to datascrubbers
17#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
18pub enum Pii {
19    /// The field will be stripped by default
20    True,
21    /// The field cannot be stripped at all
22    False,
23    /// The field will only be stripped when addressed with a specific path selector, but generic
24    /// selectors such as `$string` do not apply.
25    Maybe,
26}
27
28/// Under which names an attribute should be saved.
29#[derive(Debug, Clone, Copy)]
30pub enum WriteBehavior {
31    /// Save the attribute under its current name.
32    ///
33    /// This is the only choice for attributes that aren't deprecated.
34    CurrentName,
35    /// Save the attribute under its replacement name instead.
36    NewName(&'static str),
37    /// Save the attribute under both its current name and
38    /// its replacement name.
39    BothNames(&'static str),
40}
41
42/// Information about an attribute, as defined in `sentry-conventions`.
43#[derive(Debug, Clone)]
44pub struct AttributeInfo {
45    /// How this attribute should be saved.
46    pub write_behavior: WriteBehavior,
47    /// Whether this attribute can contain PII.
48    pub pii: Pii,
49    /// Other attribute names that alias to this attribute.
50    pub aliases: &'static [&'static str],
51}
52
53/// Returns information about an attribute, as defined in `sentry-conventions`.
54pub fn attribute_info(key: &str) -> Option<&'static AttributeInfo> {
55    ATTRIBUTES.find(key)
56}
57
58/// Special path segment in attribute keys that matches any value.
59const PLACEHOLDER_SEGMENT: &str = "<key>";
60
61struct Node<T: 'static> {
62    info: Option<T>,
63    children: phf::Map<&'static str, Node<T>>,
64}
65
66impl<T> Node<T> {
67    fn find(&self, key: &str) -> Option<&T> {
68        if key.is_empty() {
69            return self.info.as_ref();
70        }
71        let (prefix, suffix) = key.split_once('.').unwrap_or((key, ""));
72        for candidate in [prefix, PLACEHOLDER_SEGMENT] {
73            if let Some(info) = self
74                .children
75                .get(candidate)
76                .and_then(|child| child.find(suffix))
77            {
78                return Some(info);
79            }
80        }
81        None
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use std::collections::HashMap;
88
89    use phf::phf_map;
90    use relay_protocol::{Getter, Val};
91
92    use super::*;
93
94    #[test]
95    fn test_http_response_content_length() {
96        let info = attribute_info("http.response_content_length").unwrap();
97
98        insta::assert_debug_snapshot!(info, @r###"
99        AttributeInfo {
100            write_behavior: BothNames(
101                "http.response.body.size",
102            ),
103            pii: False,
104            aliases: [
105                "http.response.body.size",
106                "http.response.header.content-length",
107            ],
108        }
109        "###);
110    }
111
112    #[test]
113    fn test_url_path_parameter() {
114        // See https://github.com/getsentry/sentry-conventions/blob/d80504a40ba3a0a23eb746e2608425cf8d8e68bf/model/attributes/url/url__path__parameter__%5Bkey%5D.json.
115        let info = attribute_info("url.path.parameter.'id=123'").unwrap();
116
117        insta::assert_debug_snapshot!(info, @r###"
118        AttributeInfo {
119            write_behavior: CurrentName,
120            pii: Maybe,
121            aliases: [
122                "params.<key>",
123            ],
124        }
125        "###);
126    }
127
128    const ROOT: Node<u8> = Node {
129        info: None,
130        children: phf_map! {
131            "foo" => Node {
132                info: Some(1),
133                children: phf_map!{}
134            },
135            "<key>" =>  Node {
136                info: None,
137                children: phf_map!{
138                    "bar" => Node {
139                        info: Some(2),
140                        children: phf_map! {}
141                    }
142                }
143            }
144        },
145    };
146
147    #[test]
148    fn test_hypothetical() {
149        assert_eq!(ROOT.find("foo.bar"), Some(&2));
150    }
151
152    struct GetterMap<'a>(HashMap<&'a str, Val<'a>>);
153
154    impl Getter for GetterMap<'_> {
155        fn get_value(&self, path: &str) -> Option<Val<'_>> {
156            self.0.get(path).copied()
157        }
158    }
159
160    mod test_name_fn {
161        include!(concat!(env!("OUT_DIR"), "/test_name_fn.rs"));
162    }
163    use test_name_fn::name_for_op_and_attributes;
164
165    #[test]
166    fn only_literal_template() {
167        let attributes = GetterMap(HashMap::new());
168        assert_eq!(
169            name_for_op_and_attributes("op_with_literal_name", &attributes,),
170            "literal name"
171        );
172    }
173
174    #[test]
175    fn multiple_ops_same_template() {
176        let attributes = GetterMap(HashMap::from([("attr1", Val::String("foo"))]));
177        assert_eq!(
178            name_for_op_and_attributes("op_with_attributes_1", &attributes),
179            "foo"
180        );
181        assert_eq!(
182            name_for_op_and_attributes("op_with_attributes_2", &attributes),
183            "foo"
184        );
185    }
186
187    #[test]
188    fn skips_templates_when_attrs_are_missing() {
189        let attributes = GetterMap(HashMap::from([
190            ("attr2", Val::String("bar")),
191            ("attr3", Val::String("baz")),
192        ]));
193        assert_eq!(
194            name_for_op_and_attributes("op_with_attributes_1", &attributes),
195            "bar baz"
196        );
197    }
198
199    #[test]
200    fn handles_literal_prefixes_and_suffixes() {
201        let attributes = GetterMap(HashMap::from([("attr3", Val::String("baz"))]));
202        assert_eq!(
203            name_for_op_and_attributes("op_with_attributes_1", &attributes),
204            "prefix baz suffix",
205        );
206    }
207
208    #[test]
209    fn considers_multiple_files() {
210        let attributes = GetterMap(HashMap::new());
211        assert_eq!(
212            name_for_op_and_attributes("op_in_second_name_file", &attributes),
213            "second file literal name",
214        );
215    }
216
217    #[test]
218    fn falls_back_to_op_for_unknown_ops() {
219        let attributes = GetterMap(HashMap::new());
220        assert_eq!(
221            name_for_op_and_attributes("unknown_op", &attributes),
222            "unknown_op",
223        );
224    }
225
226    #[test]
227    fn handles_multiple_value_types() {
228        let attributes = GetterMap(HashMap::from([("attr1", Val::Bool(true))]));
229        assert_eq!(
230            name_for_op_and_attributes("op_with_attributes_1", &attributes),
231            "true",
232        );
233
234        let attributes = GetterMap(HashMap::from([("attr1", Val::I64(123))]));
235        assert_eq!(
236            name_for_op_and_attributes("op_with_attributes_1", &attributes),
237            "123",
238        );
239
240        let attributes = GetterMap(HashMap::from([("attr1", Val::U64(123))]));
241        assert_eq!(
242            name_for_op_and_attributes("op_with_attributes_1", &attributes),
243            "123",
244        );
245
246        let attributes = GetterMap(HashMap::from([("attr1", Val::F64(1.23))]));
247        assert_eq!(
248            name_for_op_and_attributes("op_with_attributes_1", &attributes),
249            "1.23",
250        );
251    }
252}