reporting_api/lib.rs
1// -*- coding: utf-8 -*-
2// ------------------------------------------------------------------------------------------------
3// Copyright © 2019, rs-reporting-api authors.
4//
5// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
6// in compliance with the License. You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software distributed under the
11// License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12// express or implied. See the License for the specific language governing permissions and
13// limitations under the License.
14// ------------------------------------------------------------------------------------------------
15
16//! This crate provides some useful Rust code for working with the [Reporting API][] and [Network
17//! Error Logging][] W3C draft specifications.
18//!
19//! [Reporting API]: https://w3c.github.io/reporting/
20//! [Network Error Logging]: https://w3c.github.io/network-error-logging/
21//!
22//! # Overview
23//!
24//! The core of the [Reporting API][] is pretty simple: reports are uploaded via a `POST` to a URL
25//! of your choosing. The payload of the `POST` request is a JSON-encoded array of reports, and
26//! the report schema is defined by the spec.
27//!
28//! The [Reporting API][] can be used to upload many different _kinds_ of reports. For instance,
29//! Reporting itself defines [crash reports][], [deprecations][], and [interventions][], all of
30//! which come from the JavaScript environment running in the browser. Other report types are
31//! complex enough that they need to be defined in their own specs, such as [Network Error
32//! Logging][] and [Content Security Policy][]. Regardless of where they're defined, each report
33//! type defines some fields specific to that type (the **_body_**), and the [Reporting API][]
34//! defines some fields that are common to all types.
35//!
36//! [crash reports]: https://w3c.github.io/reporting/#crash-report
37//! [deprecations]: https://w3c.github.io/reporting/#deprecation-report
38//! [interventions]: https://w3c.github.io/reporting/#intervention-report
39//! [Content Security Policy]: https://www.w3.org/TR/CSP3/
40//!
41//! This library provides a definition of all of these schemas as regular Rust types, along with
42//! the ability to use [serde][] to serialize and deserialize them. We've carefully defined
43//! everything so that [serde_json][] will automatically do The Right Thing and use a JSON
44//! serialization that lines up with the various specifications. We also provide way to define
45//! body schemas for new report types, and have them seamlessly fit in with the rest of the
46//! serialization logic.
47//!
48//! [serde]: https://docs.rs/serde/
49//! [serde_json]: https://docs.rs/serde_json/
50//!
51//! # Collecting reports
52//!
53//! The simplest way to use this library is if you just want to receive reports from somewhere
54//! (you're implementing a collector, for instance, and we've already defined Rust types for all of
55//! the report types that you care about).
56//!
57//! To do that, you just need to use `serde_json` to deserialize the content of the JSON string
58//! that you've received:
59//!
60//! ```
61//! # use reporting_api::BareReport;
62//! # let payload = r#"[{"age":500,"type":"network-error","url":"https://example.com/about/","user_agent":"Mozilla/5.0","body":{"referrer":"https://example.com/","sampling_fraction":0.5,"server_ip":"203.0.113.75","protocol":"h2","method":"POST","status_code":200,"elapsed_time":45,"phase":"application","type":"ok"}}]"#;
63//! let reports: Vec<BareReport> = serde_json::from_str(payload).unwrap();
64//! ```
65//!
66//! That's it! The elements of the vector will represent each of the reports in this upload batch.
67//! Each one is a "bare" report, which means that we haven't tried to figure out what type of
68//! report this is, or which Rust type corresponds with that report type. Instead, the raw body of
69//! the report is available (in the [`body`][] field) as a `serde_json` [`Value`][].
70//!
71//! If you know which particular kind of report you want to process, you can use the bare report's
72//! [`parse`][] method to convert it into a "parsed" report. For instance, if you know you only
73//! care about [Network Error Logging][] reports:
74//!
75//! ```
76//! # use reporting_api::BareReport;
77//! # use reporting_api::Report;
78//! # use reporting_api::NEL;
79//! # let payload = r#"[{"age":500,"type":"network-error","url":"https://example.com/about/","user_agent":"Mozilla/5.0","body":{"referrer":"https://example.com/","sampling_fraction":0.5,"server_ip":"203.0.113.75","protocol":"h2","method":"POST","status_code":200,"elapsed_time":45,"phase":"application","type":"ok"}}]"#;
80//! # let reports: Vec<BareReport> = serde_json::from_str(payload).unwrap();
81//! // Ignore both kinds of failure, returning a Vec<Report<NEL>>.
82//! let nel_reports = reports
83//! .into_iter()
84//! .filter_map(BareReport::parse::<NEL>)
85//! .filter_map(Result::ok)
86//! .collect::<Vec<Report<NEL>>>();
87//! ```
88//!
89//! [`BareReport`]: struct.BareReport.html
90//! [`body`]: struct.BareReport.html#structfield.body
91//! [`Value`]: https://docs.rs/serde_json/*/serde_json/value/enum.Value.html
92//! [`parse`]: struct.BareReport.html#method.parse
93//!
94//! Note that [`parse`][]'s return value is wrapped in _both_ [`Option`][] _and_ [`Result`][]. The
95//! outer [`Option`][] tells you whether or not the report is of the expected type. If it is, the
96//! inner [`Result`][] tells you whether we were able to parse the reports `body` field according
97//! to that type's expected schema. In this example, we therefore need two `filter_map` calls to
98//! strip away any mismatches and errors, leaving us with a vector of `Report<NEL>` instances.
99//!
100//! [`Option`]: https://doc.rust-lang.org/std/option/enum.Option.html
101//! [`Result`]: https://doc.rust-lang.org/std/result/enum.Result.html
102//!
103//! # Creating a new report type
104//!
105//! This should be a relatively rare occurrence, but consider a new report type that uses the
106//! [Reporting API][] but that isn't covered here. For instance, let's say there's a new `lint`
107//! report type whose body content looks like:
108//!
109//! ``` json
110//! {
111//! "source_file": "foo.js",
112//! "line": 10,
113//! "column": 12,
114//! "finding": "Indentation doesn't match the rest of the file"
115//! }
116//! ```
117//!
118//! First you'll define a Rust type to hold the body content:
119//!
120//! ```
121//! # use serde::Deserialize;
122//! # use serde::Serialize;
123//! #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
124//! pub struct Lint {
125//! pub source_file: String,
126//! pub line: u32,
127//! pub column: u32,
128//! pub finding: String,
129//! }
130//! ```
131//!
132//! Lastly, you must implement the [`ReportType`][] trait for your new type, which defines the
133//! value of the `type` field in the report payload that corresponds to this new report type.
134//!
135//! [`ReportType`]: trait.ReportType.html
136//!
137//! ```
138//! # use reporting_api::ReportType;
139//! # pub struct Lint;
140//! impl ReportType for Lint {
141//! fn report_type() -> &'static str {
142//! "lint"
143//! }
144//! }
145//! ```
146//!
147//! And that's it! The [`parse`][] method will now work with your new report type.
148
149use std::time::Duration;
150
151use serde::Deserialize;
152use serde::Serialize;
153use serde_json::Value;
154
155/// Represents a single report uploaded via the Reporting API, whose body is still a JSON object
156/// and has not yet been parsed into a more specific Rust type.
157#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
158pub struct BareReport {
159 /// The amount of time between when the report was generated by the user agent and when it was
160 /// uploaded.
161 #[serde(with = "parse_milliseconds")]
162 pub age: Duration,
163 /// The URL of the request that this report describes.
164 pub url: String,
165 /// The value of the `User-Agent` header of the request that this report describes.
166 pub user_agent: String,
167 /// The type of report
168 #[serde(rename = "type")]
169 pub report_type: String,
170 /// The body of the report, still encoded as a JSON object.
171 pub body: Value,
172}
173
174impl BareReport {
175 /// Verifies that a bare report has a particular type, and tries to parse the report body using
176 /// the corresponding Rust type. Returns `Some(Ok(...))` if everything goes well. Returns
177 /// `None` if the report has a different type, and `Some(Err(...))` if the report has the right
178 /// type but we can't parse the report body using that type's schema.
179 pub fn parse<C>(self) -> Option<Result<Report<C>, serde_json::Error>>
180 where
181 C: ReportType + for<'de> Deserialize<'de>,
182 {
183 if self.report_type != C::report_type() {
184 return None;
185 }
186 Some(self.parse_body())
187 }
188
189 fn parse_body<C>(self) -> Result<Report<C>, serde_json::Error>
190 where
191 C: for<'de> Deserialize<'de>,
192 {
193 Ok(Report {
194 age: self.age,
195 url: self.url,
196 user_agent: self.user_agent,
197 body: serde_json::from_value(self.body)?,
198 })
199 }
200}
201
202/// Represents a single report, after having parsed the body into the Rust type specific to this
203/// type of report.
204#[derive(Clone, Debug, Default, PartialEq)]
205pub struct Report<C> {
206 /// The amount of time between when the report was generated by the user agent and when it was
207 /// uploaded.
208 pub age: Duration,
209 /// The URL of the request that this report describes.
210 pub url: String,
211 /// The value of the `User-Agent` header of the request that this report describes.
212 pub user_agent: String,
213 /// The body of the report.
214 pub body: C,
215}
216
217/// A trait that maps each Rust report type to the corresponding `type` value that appears in a
218/// JSON report payload.
219pub trait ReportType {
220 /// The value of the report's `type` field for reports of this type.
221 fn report_type() -> &'static str;
222}
223
224/// The body of a single Network Error Logging report.
225#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
226pub struct NEL {
227 /// The referrer information for the request, as determined by the referrer policy associated
228 /// with its client.
229 pub referrer: String,
230 /// The sampling rate that was in effect for this request, expressed as a frcation between 0.0
231 /// and 1.0 (inclusive).
232 pub sampling_fraction: f32,
233 /// The IP address of the host to which the user agent sent the request.
234 pub server_ip: String,
235 /// The ALPN ID of the network protocol used to fetch the resource.
236 pub protocol: String,
237 /// The method of the HTTP request (e.g., `GET`, `POST`)
238 pub method: String,
239 /// The status code of the HTTP response, if available.
240 pub status_code: Option<u16>,
241 /// The elapsed time between the start of the resource fetch and when it was completed or
242 /// aborted by the user agent.
243 #[serde(with = "parse_opt_milliseconds")]
244 pub elapsed_time: Option<Duration>,
245 /// The phase of the request in which the failure occurred, if any. One of `dns`,
246 /// `connection`, or `application`. A successful request always has a phase of `application`.
247 pub phase: String,
248 /// The code describing the error that occurred, or `ok` if the request was successful. See
249 /// the NEL spec for the [authoritative
250 /// list](https://w3c.github.io/network-error-logging/#predefined-network-error-types) of
251 /// possible codes.
252 #[serde(rename = "type")]
253 pub status: String,
254}
255
256impl ReportType for NEL {
257 fn report_type() -> &'static str {
258 "network-error"
259 }
260}
261
262/// A serde parsing module that can be used to parse durations expressed as an integer number of
263/// milliseconds.
264pub mod parse_milliseconds {
265 use std::time::Duration;
266
267 use serde::Deserialize;
268 use serde::Deserializer;
269 use serde::Serializer;
270
271 pub fn serialize<S>(value: &Duration, serializer: S) -> Result<S::Ok, S::Error>
272 where
273 S: Serializer,
274 {
275 serializer.serialize_u64(value.as_millis() as u64)
276 }
277
278 pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
279 where
280 D: Deserializer<'de>,
281 {
282 Ok(Duration::from_millis(u64::deserialize(deserializer)?))
283 }
284}
285
286/// A serde parsing module that can be used to parse _optional_ durations expressed as an integer
287/// number of milliseconds.
288pub mod parse_opt_milliseconds {
289 use std::time::Duration;
290
291 use serde::Deserialize;
292 use serde::Deserializer;
293 use serde::Serializer;
294
295 pub fn serialize<S>(value: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
296 where
297 S: Serializer,
298 {
299 match value {
300 Some(duration) => serializer.serialize_some(&(duration.as_millis() as u64)),
301 None => serializer.serialize_none(),
302 }
303 }
304
305 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
306 where
307 D: Deserializer<'de>,
308 {
309 Ok(Option::<u64>::deserialize(deserializer)?.map(Duration::from_millis))
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 use serde_json::json;
318
319 #[test]
320 fn can_parse_unknown_report_type() {
321 let report_json = json!({
322 "age": 500,
323 "url": "https://example.com/about/",
324 "user_agent": "Mozilla/5.0",
325 "type": "unknown",
326 "body": {},
327 });
328 let report: BareReport =
329 serde_json::from_value(report_json).expect("Should be able to parse JSON report");
330 assert_eq!(
331 report,
332 BareReport {
333 age: Duration::from_millis(500),
334 url: "https://example.com/about/".to_string(),
335 user_agent: "Mozilla/5.0".to_string(),
336 report_type: "unknown".to_string(),
337 body: json!({}),
338 }
339 );
340 }
341
342 #[test]
343 fn cannot_parse_missing_report_type() {
344 let report_json = json!({
345 "age": 500,
346 "url": "https://example.com/about/",
347 "user_agent": "Mozilla/5.0",
348 "body": {},
349 });
350 assert!(serde_json::from_value::<BareReport>(report_json).is_err());
351 }
352
353 #[test]
354 fn cannot_parse_missing_body() {
355 let report_json = json!({
356 "age": 500,
357 "url": "https://example.com/about/",
358 "user_agent": "Mozilla/5.0",
359 "type": "unknown",
360 });
361 assert!(serde_json::from_value::<BareReport>(report_json).is_err());
362 }
363
364 #[test]
365 fn can_parse_nel_report() {
366 let report_json = json!({
367 "age": 500,
368 "type": "network-error",
369 "url": "https://example.com/about/",
370 "user_agent": "Mozilla/5.0",
371 "body": {
372 "referrer": "https://example.com/",
373 "sampling_fraction": 0.5,
374 "server_ip": "203.0.113.75",
375 "protocol": "h2",
376 "method": "POST",
377 "status_code": 200,
378 "elapsed_time": 45,
379 "phase":"application",
380 "type": "ok"
381 }
382 });
383 let bare_report: BareReport =
384 serde_json::from_value(report_json).expect("Should be able to parse JSON report");
385 let report: Report<NEL> = bare_report
386 .parse()
387 .expect("Report should be a NEL report")
388 .expect("Should be able to parse NEL report body");
389 assert_eq!(
390 report,
391 Report {
392 age: Duration::from_millis(500),
393 url: "https://example.com/about/".to_string(),
394 user_agent: "Mozilla/5.0".to_string(),
395 body: NEL {
396 referrer: "https://example.com/".to_string(),
397 sampling_fraction: 0.5,
398 server_ip: "203.0.113.75".to_string(),
399 protocol: "h2".to_string(),
400 method: "POST".to_string(),
401 status_code: Some(200),
402 elapsed_time: Some(Duration::from_millis(45)),
403 phase: "application".to_string(),
404 status: "ok".to_string(),
405 },
406 }
407 );
408 }
409}