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#[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.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 && let Value::String(s) = value.clone().into_value()
48 {
49 string_value = Some(s);
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 if let Some(value) = value.value_mut()
145 && value.value_type().contains(ValueType::OurLog)
146 {
147 return BTreeSet::new();
148 }
149
150 processor::process_value(value, &mut processor, ProcessingState::root())
151 .expect("This processor is supposed to be infallible");
152
153 processor.selectors
154}
155
156#[cfg(test)]
157mod tests {
158 use relay_event_schema::protocol::{Event, OurLog};
159
160 use super::*;
161
162 #[test]
163 fn test_empty() {
164 let mut event =
166 Annotated::<Event>::from_json(r#"{"logentry": {"message": "hi"}}"#).unwrap();
167
168 let selectors = selector_suggestions_from_value(&mut event);
169 assert!(selectors.is_empty());
170 }
171
172 #[test]
173 fn test_full() {
174 let mut event = Annotated::<Event>::from_json(
175 r#"
176 {
177 "message": "hi",
178 "exception": {
179 "values": [
180 {
181 "type": "ZeroDivisionError",
182 "value": "Divided by zero",
183 "stacktrace": {
184 "frames": [
185 {
186 "abs_path": "foo/bar/baz",
187 "filename": "baz",
188 "vars": {
189 "foo": "bar"
190 }
191 }
192 ]
193 }
194 },
195 {
196 "type": "BrokenException",
197 "value": "Something failed",
198 "stacktrace": {
199 "frames": [
200 {
201 "vars": {
202 "bam": "bar"
203 }
204 }
205 ]
206 }
207 }
208 ]
209 },
210 "extra": {
211 "My Custom Value": "123"
212 },
213 "request": {
214 "headers": {
215 "Authorization": "not really"
216 }
217 }
218 }
219 "#,
220 )
221 .unwrap();
222
223 let selectors = selector_suggestions_from_value(&mut event);
224 insta::assert_json_snapshot!(selectors, @r###"
225 [
226 {
227 "path": "$string",
228 "value": "123"
229 },
230 {
231 "path": "$string",
232 "value": "Divided by zero"
233 },
234 {
235 "path": "$string",
236 "value": "Something failed"
237 },
238 {
239 "path": "$string",
240 "value": "bar"
241 },
242 {
243 "path": "$string",
244 "value": "hi"
245 },
246 {
247 "path": "$string",
248 "value": "not really"
249 },
250 {
251 "path": "$error.value",
252 "value": "Divided by zero"
253 },
254 {
255 "path": "$error.value",
256 "value": "Something failed"
257 },
258 {
259 "path": "$frame.abs_path",
260 "value": "foo/bar/baz"
261 },
262 {
263 "path": "$frame.filename",
264 "value": "baz"
265 },
266 {
267 "path": "$frame.vars",
268 "value": null
269 },
270 {
271 "path": "$frame.vars.bam",
272 "value": "bar"
273 },
274 {
275 "path": "$frame.vars.foo",
276 "value": "bar"
277 },
278 {
279 "path": "$http.headers",
280 "value": null
281 },
282 {
283 "path": "$http.headers.Authorization",
284 "value": "not really"
285 },
286 {
287 "path": "$message",
288 "value": "hi"
289 },
290 {
291 "path": "extra",
292 "value": null
293 },
294 {
295 "path": "extra.'My Custom Value'",
296 "value": "123"
297 }
298 ]
299 "###);
300 }
301
302 #[test]
303 fn test_attributes() {
304 let mut annotated_log = Annotated::<OurLog>::from_json(
305 r#"
306 {
307 "timestamp": 1544719860.0,
308 "trace_id": "5b8efff798038103d269b633813fc60c",
309 "span_id": "eee19b7ec3c1b174",
310 "level": "info",
311 "body": "Test",
312 "attributes": {
313 "example_attribute": {
314 "type": "string",
315 "value": "abc123"
316 }
317 }
318 }"#,
319 )
320 .unwrap();
321
322 let selectors = selector_suggestions_from_value(&mut annotated_log);
323 insta::assert_json_snapshot!(selectors, @"[]");
324 }
325}