relay_pii/
generate_selectors.rs1use 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#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize)]
15pub struct SelectorSuggestion {
16 pub path: SelectorSpec,
18 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 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 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 ValueType::Object | ValueType::Array => {}
73
74 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 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
123pub 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 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}