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