relay_event_schema/protocol/
transaction.rs

1use std::fmt;
2use std::str::FromStr;
3
4use relay_protocol::{Annotated, Empty, ErrorKind, FromValue, IntoValue, SkipSerialization, Value};
5use serde::{Deserialize, Serialize};
6
7use crate::processor::ProcessValue;
8use crate::protocol::Timestamp;
9
10/// Describes how the name of the transaction was determined.
11#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
12#[serde(rename_all = "kebab-case")]
13#[derive(Default)]
14pub enum TransactionSource {
15    /// User-defined name set through `set_transaction_name`.
16    Custom,
17    /// Raw URL, potentially containing identifiers.
18    Url,
19    /// Parametrized URL or route.
20    Route,
21    /// Name of the view handling the request.
22    View,
23    /// Named after a software component, such as a function or class name.
24    Component,
25    /// The transaction name was updated to reduce the name cardinality.
26    Sanitized,
27    /// Name of a background task (e.g. a Celery task).
28    Task,
29    /// This is the default value set by Relay for legacy SDKs.
30    #[default]
31    Unknown,
32    /// Any other unknown source that is not explicitly defined above.
33    Other(String),
34}
35
36impl TransactionSource {
37    pub fn as_str(&self) -> &str {
38        match self {
39            Self::Custom => "custom",
40            Self::Url => "url",
41            Self::Route => "route",
42            Self::View => "view",
43            Self::Component => "component",
44            Self::Sanitized => "sanitized",
45            Self::Task => "task",
46            Self::Unknown => "unknown",
47            Self::Other(s) => s,
48        }
49    }
50}
51
52impl FromStr for TransactionSource {
53    type Err = std::convert::Infallible;
54
55    fn from_str(s: &str) -> Result<Self, Self::Err> {
56        match s {
57            "custom" => Ok(Self::Custom),
58            "url" => Ok(Self::Url),
59            "route" => Ok(Self::Route),
60            "view" => Ok(Self::View),
61            "component" => Ok(Self::Component),
62            "sanitized" => Ok(Self::Sanitized),
63            "task" => Ok(Self::Task),
64            "unknown" => Ok(Self::Unknown),
65            s => Ok(Self::Other(s.to_owned())),
66        }
67    }
68}
69
70impl fmt::Display for TransactionSource {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        write!(f, "{}", self.as_str())
73    }
74}
75
76impl Empty for TransactionSource {
77    #[inline]
78    fn is_empty(&self) -> bool {
79        matches!(self, Self::Unknown)
80    }
81}
82
83impl FromValue for TransactionSource {
84    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
85        match String::from_value(value) {
86            Annotated(Some(value), mut meta) => match value.parse() {
87                Ok(source) => Annotated(Some(source), meta),
88                Err(_) => {
89                    meta.add_error(ErrorKind::InvalidData);
90                    meta.set_original_value(Some(value));
91                    Annotated(None, meta)
92                }
93            },
94            Annotated(None, meta) => Annotated(None, meta),
95        }
96    }
97}
98
99impl IntoValue for TransactionSource {
100    fn into_value(self) -> Value
101    where
102        Self: Sized,
103    {
104        Value::String(match self {
105            Self::Other(s) => s,
106            _ => self.as_str().to_owned(),
107        })
108    }
109
110    fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
111    where
112        Self: Sized,
113        S: serde::Serializer,
114    {
115        serde::Serialize::serialize(self.as_str(), s)
116    }
117}
118
119impl ProcessValue for TransactionSource {}
120
121#[derive(Clone, Debug, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
122pub struct TransactionNameChange {
123    /// Describes how the previous transaction name was determined.
124    pub source: Annotated<TransactionSource>,
125
126    /// The number of propagations from the start of the transaction to this change.
127    pub propagations: Annotated<u64>,
128
129    /// Timestamp when the transaction name was changed.
130    ///
131    /// This adheres to the event timestamp specification.
132    pub timestamp: Annotated<Timestamp>,
133}
134
135/// Additional information about the name of the transaction.
136#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
137pub struct TransactionInfo {
138    /// Describes how the name of the transaction was determined.
139    ///
140    /// This will be used by the server to decide whether or not to scrub identifiers from the
141    /// transaction name, or replace the entire name with a placeholder.
142    pub source: Annotated<TransactionSource>,
143
144    /// The unmodified transaction name as obtained by the source.
145    ///
146    /// This value will only be set if the transaction name was modified during event processing.
147    #[metastructure(max_chars = 200, trim_whitespace = true)]
148    pub original: Annotated<String>,
149
150    /// A list of changes prior to the final transaction name.
151    ///
152    /// This list must be empty if the transaction name is set at the beginning of the transaction
153    /// and never changed. There is no placeholder entry for the initial transaction name.
154    pub changes: Annotated<Vec<Annotated<TransactionNameChange>>>,
155
156    /// The total number of propagations during the transaction.
157    pub propagations: Annotated<u64>,
158}
159
160#[cfg(test)]
161mod tests {
162    use chrono::{TimeZone, Utc};
163    use similar_asserts::assert_eq;
164
165    use super::*;
166
167    #[test]
168    fn test_other_source_roundtrip() {
169        let json = r#""something-new""#;
170        let source = Annotated::new(TransactionSource::Other("something-new".to_owned()));
171
172        assert_eq!(source, Annotated::from_json(json).unwrap());
173        assert_eq!(json, source.payload_to_json_pretty().unwrap());
174    }
175
176    #[test]
177    fn test_transaction_info_roundtrip() {
178        let json = r#"{
179  "source": "route",
180  "original": "/auth/login/john123/",
181  "changes": [
182    {
183      "source": "url",
184      "propagations": 1,
185      "timestamp": 946684800.0
186    }
187  ],
188  "propagations": 2
189}"#;
190
191        let info = Annotated::new(TransactionInfo {
192            source: Annotated::new(TransactionSource::Route),
193            original: Annotated::new("/auth/login/john123/".to_owned()),
194            changes: Annotated::new(vec![Annotated::new(TransactionNameChange {
195                source: Annotated::new(TransactionSource::Url),
196                propagations: Annotated::new(1),
197                timestamp: Annotated::new(
198                    Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
199                ),
200            })]),
201            propagations: Annotated::new(2),
202        });
203
204        assert_eq!(info, Annotated::from_json(json).unwrap());
205        assert_eq!(json, info.to_json_pretty().unwrap());
206    }
207}