Skip to main content

relay_event_schema/protocol/
nel.rs

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