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?.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 super::*;
237
238 #[test]
239 fn parse_full() {
240 let json = include_str!("../tests/fixtures/dynamic_sampling_context.json");
241 serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
242 }
243
244 #[test]
245 fn parse_user() {
247 let jsons = [
248 r#"{
249 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
250 "public_key": "abd0f232775f45feab79864e580d160b",
251 "user": {
252 "id": "some-id",
253 "segment": "all"
254 }
255 }"#,
256 r#"{
257 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
258 "public_key": "abd0f232775f45feab79864e580d160b",
259 "user_id": "some-id",
260 "user_segment": "all"
261 }"#,
262 r#"{
266 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
267 "public_key": "abd0f232775f45feab79864e580d160b",
268 "user_id": "",
269 "user_segment": "",
270 "user": {
271 "id": "some-id",
272 "segment": "all"
273 }
274 }"#,
275 r#"{
276 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
277 "public_key": "abd0f232775f45feab79864e580d160b",
278 "user_id": "some-id",
279 "user_segment": "all",
280 "user": {
281 "id": "bogus-id",
282 "segment": "nothing"
283 }
284 }"#,
285 r#"{
286 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
287 "public_key": "abd0f232775f45feab79864e580d160b",
288 "user_id": "some-id",
289 "user_segment": "all",
290 "user": null
291 }"#,
292 ];
293
294 for json in jsons {
295 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
296 assert_eq!(dsc.user.user_id, "some-id");
297 assert_eq!(dsc.user.user_segment, "all");
298 }
299 }
300
301 #[test]
302 fn test_parse_user_partial() {
303 let json = r#"
306 {
307 "trace_id": "b1e2a9dc-9b8e-4cd0-af0e-80e6b83b56e6",
308 "public_key": "abd0f232775f45feab79864e580d160b",
309 "user_id": "hello",
310 "user": {
311 "segment": "all"
312 }
313 }
314 "#;
315 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
316 insta::assert_ron_snapshot!(dsc, @r#"
317 {
318 "trace_id": "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
319 "public_key": "abd0f232775f45feab79864e580d160b",
320 "release": None,
321 "environment": None,
322 "transaction": None,
323 "user_id": "hello",
324 "replay_id": None,
325 }
326 "#);
327 }
328
329 #[test]
330 fn test_parse_sample_rate() {
331 let json = r#"
332 {
333 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
334 "public_key": "abd0f232775f45feab79864e580d160b",
335 "user_id": "hello",
336 "sample_rate": "0.5"
337 }
338 "#;
339 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
340 insta::assert_ron_snapshot!(dsc, @r#"
341 {
342 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
343 "public_key": "abd0f232775f45feab79864e580d160b",
344 "release": None,
345 "environment": None,
346 "transaction": None,
347 "sample_rate": "0.5",
348 "user_id": "hello",
349 "replay_id": None,
350 }
351 "#);
352 }
353
354 #[test]
355 fn test_parse_sample_rate_scientific_notation() {
356 let json = r#"
357 {
358 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
359 "public_key": "abd0f232775f45feab79864e580d160b",
360 "user_id": "hello",
361 "sample_rate": "1e-5"
362 }
363 "#;
364 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
365 insta::assert_ron_snapshot!(dsc, @r#"
366 {
367 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
368 "public_key": "abd0f232775f45feab79864e580d160b",
369 "release": None,
370 "environment": None,
371 "transaction": None,
372 "sample_rate": "0.00001",
373 "user_id": "hello",
374 "replay_id": None,
375 }
376 "#);
377 }
378
379 #[test]
380 fn test_parse_sample_rate_bogus() {
381 let json = r#"
382 {
383 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
384 "public_key": "abd0f232775f45feab79864e580d160b",
385 "user_id": "hello",
386 "sample_rate": "bogus"
387 }
388 "#;
389 serde_json::from_str::<DynamicSamplingContext>(json).unwrap_err();
390 }
391
392 #[test]
393 fn test_parse_sample_rate_number() {
394 let json = r#"
395 {
396 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
397 "public_key": "abd0f232775f45feab79864e580d160b",
398 "user_id": "hello",
399 "sample_rate": 0.1
400 }
401 "#;
402 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
403 insta::assert_ron_snapshot!(dsc, @r#"
404 {
405 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
406 "public_key": "abd0f232775f45feab79864e580d160b",
407 "release": None,
408 "environment": None,
409 "transaction": None,
410 "sample_rate": "0.1",
411 "user_id": "hello",
412 "replay_id": None,
413 }
414 "#);
415 }
416
417 #[test]
418 fn test_parse_sample_rate_integer() {
419 let json = r#"
420 {
421 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
422 "public_key": "abd0f232775f45feab79864e580d160b",
423 "user_id": "hello",
424 "sample_rate": "1"
425 }
426 "#;
427 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
428 insta::assert_ron_snapshot!(dsc, @r#"
429 {
430 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
431 "public_key": "abd0f232775f45feab79864e580d160b",
432 "release": None,
433 "environment": None,
434 "transaction": None,
435 "sample_rate": "1.0",
436 "user_id": "hello",
437 "replay_id": None,
438 }
439 "#);
440 }
441
442 #[test]
443 fn test_parse_sample_rate_negative() {
444 let json = r#"
445 {
446 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
447 "public_key": "abd0f232775f45feab79864e580d160b",
448 "user_id": "hello",
449 "sample_rate": "-0.1"
450 }
451 "#;
452 serde_json::from_str::<DynamicSamplingContext>(json).unwrap_err();
453 }
454
455 #[test]
456 fn test_parse_sampled_with_incoming_boolean() {
457 let json = r#"
458 {
459 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
460 "public_key": "abd0f232775f45feab79864e580d160b",
461 "user_id": "hello",
462 "sampled": true
463 }
464 "#;
465 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
466 let dsc_as_json = serde_json::to_string_pretty(&dsc).unwrap();
467 let expected_json = r#"{
468 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
469 "public_key": "abd0f232775f45feab79864e580d160b",
470 "release": null,
471 "environment": null,
472 "transaction": null,
473 "user_id": "hello",
474 "replay_id": null,
475 "sampled": true
476}"#;
477
478 assert_eq!(dsc_as_json, expected_json);
479 }
480
481 #[test]
482 fn test_parse_sampled_with_incoming_boolean_as_string() {
483 let json = r#"
484 {
485 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
486 "public_key": "abd0f232775f45feab79864e580d160b",
487 "user_id": "hello",
488 "sampled": "false"
489 }
490 "#;
491 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
492 let dsc_as_json = serde_json::to_string_pretty(&dsc).unwrap();
493 let expected_json = r#"{
494 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
495 "public_key": "abd0f232775f45feab79864e580d160b",
496 "release": null,
497 "environment": null,
498 "transaction": null,
499 "user_id": "hello",
500 "replay_id": null,
501 "sampled": false
502}"#;
503
504 assert_eq!(dsc_as_json, expected_json);
505 }
506
507 #[test]
508 fn test_parse_sampled_with_incoming_invalid_boolean_as_string() {
509 let json = r#"
510 {
511 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
512 "public_key": "abd0f232775f45feab79864e580d160b",
513 "user_id": "hello",
514 "sampled": "tru"
515 }
516 "#;
517 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
518 let dsc_as_json = serde_json::to_string_pretty(&dsc).unwrap();
519 let expected_json = r#"{
520 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
521 "public_key": "abd0f232775f45feab79864e580d160b",
522 "release": null,
523 "environment": null,
524 "transaction": null,
525 "user_id": "hello",
526 "replay_id": null
527}"#;
528
529 assert_eq!(dsc_as_json, expected_json);
530 }
531
532 #[test]
533 fn test_parse_sampled_with_incoming_null_value() {
534 let json = r#"
535 {
536 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
537 "public_key": "abd0f232775f45feab79864e580d160b",
538 "user_id": "hello",
539 "sampled": null
540 }
541 "#;
542 let dsc = serde_json::from_str::<DynamicSamplingContext>(json).unwrap();
543 let dsc_as_json = serde_json::to_string_pretty(&dsc).unwrap();
544 let expected_json = r#"{
545 "trace_id": "67e5504410b1426f9247bb680e5fe0c8",
546 "public_key": "abd0f232775f45feab79864e580d160b",
547 "release": null,
548 "environment": null,
549 "transaction": null,
550 "user_id": "hello",
551 "replay_id": null
552}"#;
553
554 assert_eq!(dsc_as_json, expected_json);
555 }
556
557 #[test]
558 fn getter_filled() {
559 let replay_id = Uuid::new_v4();
560 let dsc = DynamicSamplingContext {
561 trace_id: "67e5504410b1426f9247bb680e5fe0c8".parse().unwrap(),
562 public_key: ProjectKey::parse("abd0f232775f45feab79864e580d160b").unwrap(),
563 release: Some("1.1.1".into()),
564 user: TraceUserContext {
565 user_segment: "user-seg".into(),
566 user_id: "user-id".into(),
567 },
568 environment: Some("prod".into()),
569 transaction: Some("transaction1".into()),
570 sample_rate: None,
571 replay_id: Some(replay_id),
572 sampled: None,
573 other: BTreeMap::new(),
574 };
575
576 assert_eq!(Some(Val::String("1.1.1")), dsc.get_value("trace.release"));
577 assert_eq!(
578 Some(Val::String("prod")),
579 dsc.get_value("trace.environment")
580 );
581 assert_eq!(Some(Val::String("user-id")), dsc.get_value("trace.user.id"));
582 assert_eq!(
583 Some(Val::String("user-seg")),
584 dsc.get_value("trace.user.segment")
585 );
586 assert_eq!(
587 Some(Val::String("transaction1")),
588 dsc.get_value("trace.transaction")
589 );
590 assert_eq!(Some(Val::Uuid(replay_id)), dsc.get_value("trace.replay_id"));
591 }
592
593 #[test]
594 fn getter_empty() {
595 let dsc = DynamicSamplingContext {
596 trace_id: "67e5504410b1426f9247bb680e5fe0c8".parse().unwrap(),
597 public_key: ProjectKey::parse("abd0f232775f45feab79864e580d160b").unwrap(),
598 release: None,
599 user: TraceUserContext::default(),
600 environment: None,
601 transaction: None,
602 sample_rate: None,
603 replay_id: None,
604 sampled: None,
605 other: BTreeMap::new(),
606 };
607 assert_eq!(None, dsc.get_value("trace.release"));
608 assert_eq!(None, dsc.get_value("trace.environment"));
609 assert_eq!(None, dsc.get_value("trace.user.id"));
610 assert_eq!(None, dsc.get_value("trace.user.segment"));
611 assert_eq!(None, dsc.get_value("trace.user.transaction"));
612 assert_eq!(None, dsc.get_value("trace.replay_id"));
613
614 let dsc = DynamicSamplingContext {
615 trace_id: "67e5504410b1426f9247bb680e5fe0c8".parse().unwrap(),
616 public_key: ProjectKey::parse("abd0f232775f45feab79864e580d160b").unwrap(),
617 release: None,
618 user: TraceUserContext::default(),
619 environment: None,
620 transaction: None,
621 sample_rate: None,
622 replay_id: None,
623 sampled: None,
624 other: BTreeMap::new(),
625 };
626 assert_eq!(None, dsc.get_value("trace.user.id"));
627 assert_eq!(None, dsc.get_value("trace.user.segment"));
628 }
629}