relay_event_schema/protocol/
nel.rs

1//! Contains definitions for the Network Error Logging (NEL) interface.
2//!
3//! See: [`crate::protocol::contexts::NelContext`].
4
5use std::fmt;
6use std::str::FromStr;
7
8use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, Value};
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12use crate::processor::ProcessValue;
13use crate::protocol::IpAddr;
14
15/// Describes which phase the error occurred in.
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ProcessValue)]
17#[serde(rename_all = "lowercase")]
18pub enum NetworkReportPhases {
19    /// The error occurred during DNS resolution.
20    DNS,
21    /// The error occurred during secure connection establishment.
22    Connections,
23    /// The error occurred during the transmission of request and response .
24    Application,
25    /// For forward-compatibility.
26    Other(String),
27}
28
29impl NetworkReportPhases {
30    /// Creates the string representation of the current enum value.
31    pub fn as_str(&self) -> &str {
32        match *self {
33            NetworkReportPhases::DNS => "dns",
34            NetworkReportPhases::Connections => "connection",
35            NetworkReportPhases::Application => "application",
36            NetworkReportPhases::Other(ref unknown) => unknown,
37        }
38    }
39}
40
41impl fmt::Display for NetworkReportPhases {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        f.write_str(self.as_str())
44    }
45}
46
47impl AsRef<str> for NetworkReportPhases {
48    fn as_ref(&self) -> &str {
49        self.as_str()
50    }
51}
52
53impl Empty for NetworkReportPhases {
54    #[inline]
55    fn is_empty(&self) -> bool {
56        false
57    }
58}
59
60/// Error parsing a [`NetworkReportPhases`].
61#[derive(Clone, Copy, Debug)]
62pub struct ParseNetworkReportPhaseError;
63
64impl fmt::Display for ParseNetworkReportPhaseError {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        write!(f, "invalid network report phase")
67    }
68}
69
70impl FromStr for NetworkReportPhases {
71    type Err = ParseNetworkReportPhaseError;
72
73    fn from_str(s: &str) -> Result<Self, Self::Err> {
74        let s = s.to_lowercase();
75        Ok(match s.as_str() {
76            "dns" => NetworkReportPhases::DNS,
77            "connection" => NetworkReportPhases::Connections,
78            "application" => NetworkReportPhases::Application,
79            _ => NetworkReportPhases::Other(s),
80        })
81    }
82}
83
84impl FromValue for NetworkReportPhases {
85    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
86        match value {
87            Annotated(Some(Value::String(value)), mut meta) => match value.parse() {
88                Ok(phase) => Annotated(Some(phase), meta),
89                Err(_) => {
90                    meta.add_error(relay_protocol::Error::expected("a string"));
91                    meta.set_original_value(Some(value));
92                    Annotated(None, meta)
93                }
94            },
95            Annotated(None, meta) => Annotated(None, meta),
96            Annotated(Some(value), mut meta) => {
97                meta.add_error(relay_protocol::Error::expected("a string"));
98                meta.set_original_value(Some(value));
99                Annotated(None, meta)
100            }
101        }
102    }
103}
104
105impl IntoValue for NetworkReportPhases {
106    fn into_value(self) -> Value {
107        Value::String(match self {
108            Self::Other(s) => s,
109            _ => self.as_str().to_owned(),
110        })
111    }
112
113    fn serialize_payload<S>(
114        &self,
115        s: S,
116        _behavior: relay_protocol::SkipSerialization,
117    ) -> Result<S::Ok, S::Error>
118    where
119        Self: Sized,
120        S: serde::Serializer,
121    {
122        Serialize::serialize(self.as_str(), s)
123    }
124}
125
126/// The NEL parsing errors.
127#[derive(Debug, Error)]
128pub enum NetworkReportError {
129    /// Incoming Json is unparsable.
130    #[error("incoming json is unparsable")]
131    InvalidJson(#[from] serde_json::Error),
132}
133
134/// Generated network error report (NEL).
135#[derive(Debug, Default, Clone, PartialEq, FromValue, IntoValue, Empty)]
136pub struct BodyRaw {
137    /// The time between the start of the resource fetch and when it was completed or aborted.
138    pub elapsed_time: Annotated<u64>,
139    /// HTTP method.
140    pub method: Annotated<String>,
141    /// If request failed, the phase of its network error. If request succeeded, "application".
142    pub phase: Annotated<NetworkReportPhases>,
143    /// The HTTP protocol and version.
144    pub protocol: Annotated<String>,
145    /// Request's referrer, as determined by the referrer policy associated with its client.
146    pub referrer: Annotated<String>,
147    /// The sampling rate.
148    pub sampling_fraction: Annotated<f64>,
149    /// The IP address of the server where the site is hosted.
150    pub server_ip: Annotated<IpAddr>,
151    /// HTTP status code.
152    pub status_code: Annotated<i64>,
153    /// If request failed, the type of its network error. If request succeeded, "ok".
154    #[metastructure(field = "type")]
155    pub ty: Annotated<String>,
156    /// For forward compatibility.
157    #[metastructure(additional_properties, pii = "maybe")]
158    pub other: Object<Value>,
159}
160
161/// Models the content of a NEL report.
162///
163/// See <https://w3c.github.io/network-error-logging/>
164#[derive(Debug, Default, Clone, PartialEq, FromValue, IntoValue, Empty)]
165pub struct NetworkReportRaw {
166    /// The age of the report since it got collected and before it got sent.
167    pub age: Annotated<i64>,
168    /// The type of the report.
169    #[metastructure(field = "type")]
170    pub ty: Annotated<String>,
171    /// The URL of the document in which the error occurred.
172    #[metastructure(pii = "true")]
173    pub url: Annotated<String>,
174    /// The User-Agent HTTP header.
175    pub user_agent: Annotated<String>,
176    /// The body of the NEL report.
177    pub body: Annotated<BodyRaw>,
178    /// For forward compatibility.
179    #[metastructure(additional_properties, pii = "maybe")]
180    pub other: Object<Value>,
181}
182
183#[cfg(test)]
184mod tests {
185    use relay_protocol::{assert_annotated_snapshot, Annotated};
186
187    use crate::protocol::NetworkReportRaw;
188
189    #[test]
190    fn test_nel_raw_basic() {
191        let json = r#"{
192            "age": 31042,
193            "body": {
194                "elapsed_time": 0,
195                "method": "GET",
196                "phase": "connection",
197                "protocol": "http/1.1",
198                "referrer": "",
199                "sampling_fraction": 1.0,
200                "server_ip": "127.0.0.1",
201                "status_code": 0,
202                "type": "tcp.refused"
203            },
204            "type": "network-error",
205            "url": "http://example.com/",
206            "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
207        }"#;
208
209        let report: Annotated<NetworkReportRaw> =
210            Annotated::from_json_bytes(json.as_bytes()).unwrap();
211
212        assert_annotated_snapshot!(report, @r###"
213        {
214          "age": 31042,
215          "type": "network-error",
216          "url": "http://example.com/",
217          "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
218          "body": {
219            "elapsed_time": 0,
220            "method": "GET",
221            "phase": "connection",
222            "protocol": "http/1.1",
223            "referrer": "",
224            "sampling_fraction": 1.0,
225            "server_ip": "127.0.0.1",
226            "status_code": 0,
227            "type": "tcp.refused"
228          }
229        }
230        "###);
231    }
232}