relay_pii/
generate_selectors.rs

1use std::collections::BTreeSet;
2
3use relay_event_schema::processor::{
4    self, Pii, ProcessValue, ProcessingResult, ProcessingState, Processor, ValueType,
5};
6use relay_event_schema::protocol::{AsPair, PairList};
7use relay_protocol::{Annotated, Meta, Value};
8use serde::Serialize;
9
10use crate::selector::{SelectorPathItem, SelectorSpec};
11use crate::utils;
12
13/// Metadata about a selector found in the event
14#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize)]
15pub struct SelectorSuggestion {
16    /// The selector that users should be able to use to address the underlying value
17    pub path: SelectorSpec,
18    /// The JSON-serialized value for previewing what the selector means.
19    ///
20    /// Right now this only contains string values.
21    pub value: Option<String>,
22}
23
24struct GenerateSelectorsProcessor {
25    selectors: BTreeSet<SelectorSuggestion>,
26}
27
28impl Processor for GenerateSelectorsProcessor {
29    fn before_process<T: ProcessValue>(
30        &mut self,
31        value: Option<&T>,
32        _meta: &mut Meta,
33        state: &ProcessingState<'_>,
34    ) -> ProcessingResult {
35        // The following skip-conditions are in sync with what the PiiProcessor does.
36        if state.value_type().contains(ValueType::Boolean)
37            || value.is_none()
38            || state.attrs().pii == Pii::False
39        {
40            return Ok(());
41        }
42
43        let mut insert_path = |path: SelectorSpec| {
44            if path.matches_path(&state.path()) {
45                let mut string_value = None;
46                if let Some(value) = value {
47                    if let Value::String(s) = value.clone().into_value() {
48                        string_value = Some(s);
49                    }
50                }
51                self.selectors.insert(SelectorSuggestion {
52                    path,
53                    value: string_value,
54                });
55                true
56            } else {
57                false
58            }
59        };
60
61        let mut path = Vec::new();
62
63        // Walk through processing state in reverse order and build selector path off of that.
64        for substate in state.iter() {
65            if !substate.entered_anything() {
66                continue;
67            }
68
69            for value_type in substate.value_type() {
70                match value_type {
71                    // $array.0.foo and $object.bar are not particularly good suggestions.
72                    ValueType::Object | ValueType::Array => {}
73
74                    // a.b.c.$string is not a good suggestion, so special case those.
75                    ty @ ValueType::String
76                    | ty @ ValueType::Number
77                    | ty @ ValueType::Boolean
78                    | ty @ ValueType::DateTime => {
79                        insert_path(SelectorSpec::Path(vec![SelectorPathItem::Type(ty)]));
80                    }
81
82                    ty => {
83                        let mut path = path.clone();
84                        path.push(SelectorPathItem::Type(ty));
85                        path.reverse();
86                        if insert_path(SelectorSpec::Path(path)) {
87                            // If we managed to generate $http.header.Authorization, we do not want to
88                            // generate request.headers.Authorization as well.
89                            return Ok(());
90                        }
91                    }
92                }
93            }
94
95            if let Some(key) = substate.path().key() {
96                path.push(SelectorPathItem::Key(key.to_owned()));
97            } else if substate.path().index().is_some() {
98                path.push(SelectorPathItem::Wildcard);
99            } else {
100                debug_assert!(substate.depth() == 0);
101                break;
102            }
103        }
104
105        if !path.is_empty() {
106            path.reverse();
107            insert_path(SelectorSpec::Path(path));
108        }
109
110        Ok(())
111    }
112
113    fn process_pairlist<T: ProcessValue + AsPair>(
114        &mut self,
115        value: &mut PairList<T>,
116        _meta: &mut Meta,
117        state: &ProcessingState,
118    ) -> ProcessingResult {
119        utils::process_pairlist(self, value, state)
120    }
121}
122
123/// Walk through a value and collect selectors that can be applied to it in a PII config. This
124/// function is used in the UI to provide auto-completion of selectors.
125///
126/// This generates a couple of duplicate suggestions such as `request.headers` and `$http.headers`
127/// in order to make it more likely that the user input starting with either prefix can be
128/// completed.
129///
130/// The main value in autocompletion is that we can complete `$http.headers.Authorization` as soon
131/// as the user types `Auth`.
132///
133/// XXX: This function should not have to take a mutable ref, we only do that due to restrictions
134/// on the Processor trait that we internally use to traverse the event.
135pub fn selector_suggestions_from_value<T: ProcessValue>(
136    value: &mut Annotated<T>,
137) -> BTreeSet<SelectorSuggestion> {
138    let mut processor = GenerateSelectorsProcessor {
139        selectors: BTreeSet::new(),
140    };
141
142    processor::process_value(value, &mut processor, ProcessingState::root())
143        .expect("This processor is supposed to be infallible");
144
145    processor.selectors
146}
147
148#[cfg(test)]
149mod tests {
150    use relay_event_schema::protocol::Event;
151
152    use super::*;
153
154    #[test]
155    fn test_empty() {
156        // Test that an event without PII will generate empty list.
157        let mut event =
158            Annotated::<Event>::from_json(r#"{"logentry": {"message": "hi"}}"#).unwrap();
159
160        let selectors = selector_suggestions_from_value(&mut event);
161        assert!(selectors.is_empty());
162    }
163
164    #[test]
165    fn test_full() {
166        let mut event = Annotated::<Event>::from_json(
167            r#"
168            {
169              "message": "hi",
170              "exception": {
171                "values": [
172                  {
173                    "type": "ZeroDivisionError",
174                    "value": "Divided by zero",
175                    "stacktrace": {
176                      "frames": [
177                        {
178                          "abs_path": "foo/bar/baz",
179                          "filename": "baz",
180                          "vars": {
181                            "foo": "bar"
182                          }
183                        }
184                      ]
185                    }
186                  },
187                  {
188                    "type": "BrokenException",
189                    "value": "Something failed",
190                    "stacktrace": {
191                      "frames": [
192                        {
193                          "vars": {
194                            "bam": "bar"
195                          }
196                        }
197                      ]
198                    }
199                  }
200                ]
201              },
202              "extra": {
203                "My Custom Value": "123"
204              },
205              "request": {
206                "headers": {
207                  "Authorization": "not really"
208                }
209              }
210            }
211            "#,
212        )
213        .unwrap();
214
215        let selectors = selector_suggestions_from_value(&mut event);
216        insta::assert_json_snapshot!(selectors, @r###"
217        [
218          {
219            "path": "$string",
220            "value": "123"
221          },
222          {
223            "path": "$string",
224            "value": "Divided by zero"
225          },
226          {
227            "path": "$string",
228            "value": "Something failed"
229          },
230          {
231            "path": "$string",
232            "value": "bar"
233          },
234          {
235            "path": "$string",
236            "value": "hi"
237          },
238          {
239            "path": "$string",
240            "value": "not really"
241          },
242          {
243            "path": "$error.value",
244            "value": "Divided by zero"
245          },
246          {
247            "path": "$error.value",
248            "value": "Something failed"
249          },
250          {
251            "path": "$frame.abs_path",
252            "value": "foo/bar/baz"
253          },
254          {
255            "path": "$frame.filename",
256            "value": "baz"
257          },
258          {
259            "path": "$frame.vars",
260            "value": null
261          },
262          {
263            "path": "$frame.vars.bam",
264            "value": "bar"
265          },
266          {
267            "path": "$frame.vars.foo",
268            "value": "bar"
269          },
270          {
271            "path": "$http.headers",
272            "value": null
273          },
274          {
275            "path": "$http.headers.Authorization",
276            "value": "not really"
277          },
278          {
279            "path": "$message",
280            "value": "hi"
281          },
282          {
283            "path": "extra",
284            "value": null
285          },
286          {
287            "path": "extra.'My Custom Value'",
288            "value": "123"
289          }
290        ]
291        "###);
292    }
293}