1use std::collections::BTreeMap;
11use std::fmt;
12
13use relay_base_schema::project::ProjectKey;
14use relay_event_schema::protocol::TraceId;
15use relay_protocol::{Getter, Val};
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use uuid::Uuid;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct DynamicSamplingContext {
27 pub trace_id: TraceId,
29 pub public_key: ProjectKey,
31 #[serde(default)]
33 pub release: Option<String>,
34 #[serde(default)]
36 pub environment: Option<String>,
37 #[serde(default, alias = "segment_name")]
42 pub transaction: Option<String>,
43 #[serde(
46 default,
47 with = "sample_rate_as_string",
48 skip_serializing_if = "Option::is_none"
49 )]
50 pub sample_rate: Option<f64>,
51 #[serde(flatten, default)]
54 pub user: TraceUserContext,
55 pub replay_id: Option<Uuid>,
57 #[serde(
59 default,
60 deserialize_with = "deserialize_bool_option",
61 skip_serializing_if = "Option::is_none"
62 )]
63 pub sampled: Option<bool>,
64 #[serde(flatten, default)]
66 pub other: BTreeMap<String, Value>,
67}
68
69impl Getter for DynamicSamplingContext {
70 fn get_value(&self, path: &str) -> Option<Val<'_>> {
71 Some(match path.strip_prefix("trace.")? {
72 "release" => self.release.as_deref()?.into(),
73 "environment" => self.environment.as_deref()?.into(),
74 "user.id" => or_none(&self.user.user_id)?.into(),
75 "user.segment" => or_none(&self.user.user_segment)?.into(),
76 "transaction" => self.transaction.as_deref()?.into(),
77 "replay_id" => self.replay_id.as_ref()?.into(),
78 _ => return None,
79 })
80 }
81}
82
83fn or_none(string: &impl AsRef<str>) -> Option<&str> {
84 match string.as_ref() {
85 "" => None,
86 other => Some(other),
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Default)]
92pub struct TraceUserContext {
93 #[serde(default, skip_serializing_if = "String::is_empty")]
95 pub user_segment: String,
96
97 #[serde(default, skip_serializing_if = "String::is_empty")]
99 pub user_id: String,
100}
101
102impl<'de> Deserialize<'de> for TraceUserContext {
103 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
104 where
105 D: serde::Deserializer<'de>,
106 {
107 #[derive(Deserialize, Default)]
108 struct Nested {
109 #[serde(default)]
110 pub segment: String,
111 #[serde(default)]
112 pub id: String,
113 }
114
115 #[derive(Deserialize)]
116 struct Helper {
117 #[serde(default)]
120 user: Option<Nested>,
121 #[serde(default)]
122 user_segment: String,
123 #[serde(default)]
124 user_id: String,
125 }
126
127 let helper = Helper::deserialize(deserializer)?;
128
129 if helper.user_id.is_empty() && helper.user_segment.is_empty() {
130 let user = helper.user.unwrap_or_default();
131 Ok(TraceUserContext {
132 user_segment: user.segment,
133 user_id: user.id,
134 })
135 } else {
136 Ok(TraceUserContext {
137 user_segment: helper.user_segment,
138 user_id: helper.user_id,
139 })
140 }
141 }
142}
143
144mod sample_rate_as_string {
145 use std::borrow::Cow;
146
147 use serde::{Deserialize, Serialize};
148
149 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
150 where
151 D: serde::Deserializer<'de>,
152 {
153 #[derive(Debug, Clone, Deserialize)]
154 #[serde(untagged)]
155 enum StringOrFloat<'a> {
156 String(#[serde(borrow)] Cow<'a, str>),
157 Float(f64),
158 }
159
160 let value = match Option::<StringOrFloat>::deserialize(deserializer)? {
161 Some(value) => value,
162 None => return Ok(None),
163 };
164
165 let parsed_value = match value {
166 StringOrFloat::Float(f) => f,
167 StringOrFloat::String(s) => {
168 serde_json::from_str(&s).map_err(serde::de::Error::custom)?
169 }
170 };
171
172 if parsed_value < 0.0 {
173 return Err(serde::de::Error::custom("sample rate cannot be negative"));
174 }
175
176 Ok(Some(parsed_value))
177 }
178
179 pub fn serialize<S>(value: &Option<f64>, serializer: S) -> Result<S::Ok, S::Error>
180 where
181 S: serde::Serializer,
182 {
183 match value {
184 Some(float) => serde_json::to_string(float)
185 .map_err(|e| serde::ser::Error::custom(e.to_string()))?
186 .serialize(serializer),
187 None => value.serialize(serializer),
188 }
189 }
190}
191
192struct BoolOptionVisitor;
193
194impl serde::de::Visitor<'_> for BoolOptionVisitor {
195 type Value = Option<bool>;
196
197 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
198 formatter.write_str("`true` or `false` as boolean or string")
199 }
200
201 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
202 where
203 E: serde::de::Error,
204 {
205 Ok(Some(v))
206 }
207
208 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
209 where
210 E: serde::de::Error,
211 {
212 Ok(match v {
213 "true" => Some(true),
214 "false" => Some(false),
215 _ => None,
216 })
217 }
218
219 fn visit_unit<E>(self) -> Result<Self::Value, E>
220 where
221 E: serde::de::Error,
222 {
223 Ok(None)
224 }
225}
226
227fn deserialize_bool_option<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
228where
229 D: serde::Deserializer<'de>,
230{
231 deserializer.deserialize_any(BoolOptionVisitor)
232}
233
234#[cfg(test)]
235mod tests {
236 use relay_protocol::HexId;
237
238 use super::*;
239
240 #[test]
241 fn parse_full() {
242 let json = include_str!("../tests/fixtures/dynamic_sampling_context.json");
243 serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
244 }
245
246 #[test]
247 fn parse_user() {
249 let jsons = [
250 r#"{
251 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
252 "public_key": "abd0f232775f45feab79864e580d160b",
253 "user": {
254 "id": "some-id",
255 "segment": "all"
256 }
257 }"#,
258 r#"{
259 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
260 "public_key": "abd0f232775f45feab79864e580d160b",
261 "user_id": "some-id",
262 "user_segment": "all"
263 }"#,
264 r#"{
268 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
269 "public_key": "abd0f232775f45feab79864e580d160b",
270 "user_id": "",
271 "user_segment": "",
272 "user": {
273 "id": "some-id",
274 "segment": "all"
275 }
276 }"#,
277 r#"{
278 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
279 "public_key": "abd0f232775f45feab79864e580d160b",
280 "user_id": "some-id",
281 "user_segment": "all",
282 "user": {
283 "id": "bogus-id",
284 "segment": "nothing"
285 }
286 }"#,
287 r#"{
288 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
289 "public_key": "abd0f232775f45feab79864e580d160b",
290 "user_id": "some-id",
291 "user_segment": "all",
292 "user": null
293 }"#,
294 ];
295
296 for json in jsons {
297 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
298 assert_eq!(dsc.user.user_id, "some-id");
299 assert_eq!(dsc.user.user_segment, "all");
300 }
301 }
302
303 #[test]
304 fn test_parse_user_partial() {
305 let json = r#"
308 {
309 "trace_id": "b1e2a9dc-9b8e-4cd0-af0e-80e6b83b56e6",
310 "public_key": "abd0f232775f45feab79864e580d160b",
311 "user_id": "hello",
312 "user": {
313 "segment": "all"
314 }
315 }
316 "#;
317 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
318 insta::assert_ron_snapshot!(dsc, @r#"
319 {
320 "trace_id": "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
321 "public_key": "abd0f232775f45feab79864e580d160b",
322 "release": None,
323 "environment": None,
324 "transaction": None,
325 "user_id": "hello",
326 "replay_id": None,
327 }
328 "#);
329 }
330
331 #[test]
332 fn test_parse_sample_rate() {
333 let json = r#"
334 {
335 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
336 "public_key": "abd0f232775f45feab79864e580d160b",
337 "user_id": "hello",
338 "sample_rate": "0.5"
339 }
340 "#;
341 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
342 insta::assert_ron_snapshot!(dsc, @r#"
343 {
344 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
345 "public_key": "abd0f232775f45feab79864e580d160b",
346 "release": None,
347 "environment": None,
348 "transaction": None,
349 "sample_rate": "0.5",
350 "user_id": "hello",
351 "replay_id": None,
352 }
353 "#);
354 }
355
356 #[test]
357 fn test_parse_sample_rate_scientific_notation() {
358 let json = r#"
359 {
360 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
361 "public_key": "abd0f232775f45feab79864e580d160b",
362 "user_id": "hello",
363 "sample_rate": "1e-5"
364 }
365 "#;
366 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
367 insta::assert_ron_snapshot!(dsc, @r#"
368 {
369 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
370 "public_key": "abd0f232775f45feab79864e580d160b",
371 "release": None,
372 "environment": None,
373 "transaction": None,
374 "sample_rate": "0.00001",
375 "user_id": "hello",
376 "replay_id": None,
377 }
378 "#);
379 }
380
381 #[test]
382 fn test_parse_sample_rate_bogus() {
383 let json = r#"
384 {
385 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
386 "public_key": "abd0f232775f45feab79864e580d160b",
387 "user_id": "hello",
388 "sample_rate": "bogus"
389 }
390 "#;
391 serde_json::from_str::<DynamicSamplingContext>(json).unwrap_err();
392 }
393
394 #[test]
395 fn test_parse_sample_rate_number() {
396 let json = r#"
397 {
398 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
399 "public_key": "abd0f232775f45feab79864e580d160b",
400 "user_id": "hello",
401 "sample_rate": 0.1
402 }
403 "#;
404 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
405 insta::assert_ron_snapshot!(dsc, @r#"
406 {
407 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
408 "public_key": "abd0f232775f45feab79864e580d160b",
409 "release": None,
410 "environment": None,
411 "transaction": None,
412 "sample_rate": "0.1",
413 "user_id": "hello",
414 "replay_id": None,
415 }
416 "#);
417 }
418
419 #[test]
420 fn test_parse_sample_rate_integer() {
421 let json = r#"
422 {
423 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
424 "public_key": "abd0f232775f45feab79864e580d160b",
425 "user_id": "hello",
426 "sample_rate": "1"
427 }
428 "#;
429 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
430 insta::assert_ron_snapshot!(dsc, @r#"
431 {
432 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
433 "public_key": "abd0f232775f45feab79864e580d160b",
434 "release": None,
435 "environment": None,
436 "transaction": None,
437 "sample_rate": "1.0",
438 "user_id": "hello",
439 "replay_id": None,
440 }
441 "#);
442 }
443
444 #[test]
445 fn test_parse_sample_rate_negative() {
446 let json = r#"
447 {
448 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
449 "public_key": "abd0f232775f45feab79864e580d160b",
450 "user_id": "hello",
451 "sample_rate": "-0.1"
452 }
453 "#;
454 serde_json::from_str::<DynamicSamplingContext>(json).unwrap_err();
455 }
456
457 #[test]
458 fn test_parse_sampled_with_incoming_boolean() {
459 let json = r#"
460 {
461 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
462 "public_key": "abd0f232775f45feab79864e580d160b",
463 "user_id": "hello",
464 "sampled": true
465 }
466 "#;
467 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
468 let dsc_as_json = serde_json::to_string_pretty(&dsc).unwrap();
469 let expected_json = r#"{
470 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
471 "public_key": "abd0f232775f45feab79864e580d160b",
472 "release": null,
473 "environment": null,
474 "transaction": null,
475 "user_id": "hello",
476 "replay_id": null,
477 "sampled": true
478}"#;
479
480 assert_eq!(dsc_as_json, expected_json);
481 }
482
483 #[test]
484 fn test_parse_sampled_with_incoming_boolean_as_string() {
485 let json = r#"
486 {
487 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
488 "public_key": "abd0f232775f45feab79864e580d160b",
489 "user_id": "hello",
490 "sampled": "false"
491 }
492 "#;
493 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
494 let dsc_as_json = serde_json::to_string_pretty(&dsc).unwrap();
495 let expected_json = r#"{
496 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
497 "public_key": "abd0f232775f45feab79864e580d160b",
498 "release": null,
499 "environment": null,
500 "transaction": null,
501 "user_id": "hello",
502 "replay_id": null,
503 "sampled": false
504}"#;
505
506 assert_eq!(dsc_as_json, expected_json);
507 }
508
509 #[test]
510 fn test_parse_sampled_with_incoming_invalid_boolean_as_string() {
511 let json = r#"
512 {
513 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
514 "public_key": "abd0f232775f45feab79864e580d160b",
515 "user_id": "hello",
516 "sampled": "tru"
517 }
518 "#;
519 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
520 let dsc_as_json = serde_json::to_string_pretty(&dsc).unwrap();
521 let expected_json = r#"{
522 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
523 "public_key": "abd0f232775f45feab79864e580d160b",
524 "release": null,
525 "environment": null,
526 "transaction": null,
527 "user_id": "hello",
528 "replay_id": null
529}"#;
530
531 assert_eq!(dsc_as_json, expected_json);
532 }
533
534 #[test]
535 fn test_parse_sampled_with_incoming_null_value() {
536 let json = r#"
537 {
538 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
539 "public_key": "abd0f232775f45feab79864e580d160b",
540 "user_id": "hello",
541 "sampled": null
542 }
543 "#;
544 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
545 let dsc_as_json = serde_json::to_string_pretty(&dsc).unwrap();
546 let expected_json = r#"{
547 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
548 "public_key": "abd0f232775f45feab79864e580d160b",
549 "release": null,
550 "environment": null,
551 "transaction": null,
552 "user_id": "hello",
553 "replay_id": null
554}"#;
555
556 assert_eq!(dsc_as_json, expected_json);
557 }
558
559 #[test]
560 fn getter_filled() {
561 let replay_id = Uuid::new_v4();
562 let dsc = DynamicSamplingContext {
563 trace_id: "67e5504410b1426f9247bb680e5fe0c8".parse().unwrap(),
564 public_key: ProjectKey::parse("abd0f232775f45feab79864e580d160b").unwrap(),
565 release: Some("1.1.1".into()),
566 user: TraceUserContext {
567 user_segment: "user-seg".into(),
568 user_id: "user-id".into(),
569 },
570 environment: Some("prod".into()),
571 transaction: Some("transaction1".into()),
572 sample_rate: None,
573 replay_id: Some(replay_id),
574 sampled: None,
575 other: BTreeMap::new(),
576 };
577
578 assert_eq!(Some(Val::String("1.1.1")), dsc.get_value("trace.release"));
579 assert_eq!(
580 Some(Val::String("prod")),
581 dsc.get_value("trace.environment")
582 );
583 assert_eq!(Some(Val::String("user-id")), dsc.get_value("trace.user.id"));
584 assert_eq!(
585 Some(Val::String("user-seg")),
586 dsc.get_value("trace.user.segment")
587 );
588 assert_eq!(
589 Some(Val::String("transaction1")),
590 dsc.get_value("trace.transaction")
591 );
592 assert_eq!(
593 Some(Val::HexId(HexId(replay_id.as_bytes()))),
594 dsc.get_value("trace.replay_id")
595 );
596 }
597
598 #[test]
599 fn getter_empty() {
600 let dsc = DynamicSamplingContext {
601 trace_id: "67e5504410b1426f9247bb680e5fe0c8".parse().unwrap(),
602 public_key: ProjectKey::parse("abd0f232775f45feab79864e580d160b").unwrap(),
603 release: None,
604 user: TraceUserContext::default(),
605 environment: None,
606 transaction: None,
607 sample_rate: None,
608 replay_id: None,
609 sampled: None,
610 other: BTreeMap::new(),
611 };
612 assert_eq!(None, dsc.get_value("trace.release"));
613 assert_eq!(None, dsc.get_value("trace.environment"));
614 assert_eq!(None, dsc.get_value("trace.user.id"));
615 assert_eq!(None, dsc.get_value("trace.user.segment"));
616 assert_eq!(None, dsc.get_value("trace.user.transaction"));
617 assert_eq!(None, dsc.get_value("trace.replay_id"));
618
619 let dsc = DynamicSamplingContext {
620 trace_id: "67e5504410b1426f9247bb680e5fe0c8".parse().unwrap(),
621 public_key: ProjectKey::parse("abd0f232775f45feab79864e580d160b").unwrap(),
622 release: None,
623 user: TraceUserContext::default(),
624 environment: None,
625 transaction: None,
626 sample_rate: None,
627 replay_id: None,
628 sampled: None,
629 other: BTreeMap::new(),
630 };
631 assert_eq!(None, dsc.get_value("trace.user.id"));
632 assert_eq!(None, dsc.get_value("trace.user.segment"));
633 }
634}