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