Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Network Error Logging #2421

Merged
merged 69 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
f863254
hack nel
oioki Aug 21, 2023
2f8a2d9
Fix Nel endpoint
olksdr Aug 21, 2023
a695391
moar hacking
oioki Aug 21, 2023
704592c
cleanup
oioki Aug 21, 2023
ae3c9cc
cleanup warnings
oioki Aug 21, 2023
6fd93e0
tiny cleanup
oioki Aug 21, 2023
2f376d2
add browser contexts from user-agent header
oioki Aug 21, 2023
a503eb3
add logger and placeholder for culprit
oioki Aug 21, 2023
78c2839
Add tags, user and make sure we have "type" in the type
olksdr Aug 22, 2023
8029b3a
fix tests
olksdr Aug 22, 2023
58ee827
Fix lint
olksdr Aug 22, 2023
0d2ffd9
Wrong user ip
olksdr Aug 22, 2023
b04fa10
Normalize NEL
olksdr Aug 22, 2023
1d8b7b6
Change the timestamp of the event
olksdr Aug 22, 2023
0c7eda5
removed culprit (it will be set on sentry side)
oioki Aug 22, 2023
fec72d9
split issues by HTTP status code
oioki Aug 22, 2023
a8d7ee1
merge master; fix conflics
oioki Sep 26, 2023
aa21e10
fixes
oioki Sep 26, 2023
be0726c
emit envelope for each NEL report item
oioki Sep 26, 2023
71f844b
Merge branch 'master' into hackweek/nel
oioki Oct 2, 2023
1c41705
remove unused vars
olksdr Oct 5, 2023
1d072c2
Merge branch 'master' into hackweek/nel
olksdr Oct 5, 2023
c8cf938
Hide JsonSchema behind the feature flag
olksdr Oct 5, 2023
7eaed71
add Nel event type
oioki Oct 5, 2023
90a8994
update test snapshot
oioki Oct 5, 2023
0686a91
add test for test_nel_basic
oioki Oct 5, 2023
d50c667
bit more docs, fix typo
oioki Oct 5, 2023
2297808
fix docs
oioki Oct 5, 2023
df467b8
do not deduplicate NEL reports
oioki Oct 5, 2023
efc6f64
add to changelog
oioki Oct 5, 2023
ebf6ee9
fix changelog
oioki Oct 5, 2023
925c42d
also add to py/CHANGELOG
oioki Oct 5, 2023
8a1f795
Use Annotated to deserialize into the data type from json
olksdr Oct 6, 2023
33e38bd
Apply suggestions from code review
olksdr Oct 6, 2023
c92aefb
Merge branch 'master' into hackweek/nel
olksdr Oct 6, 2023
d894f2a
Fix lint
olksdr Oct 6, 2023
9ca755f
Simplified
olksdr Oct 6, 2023
ed5b68f
Address review comments
olksdr Oct 6, 2023
be229ee
Merge branch 'master' into hackweek/nel
olksdr Oct 12, 2023
34f3aad
Nel is an error
olksdr Oct 12, 2023
29a4cb6
Minimal deserialization and split envelope while queueing
olksdr Oct 12, 2023
5010cad
Move code around
olksdr Oct 13, 2023
34014f0
Merge branch 'master' into hackweek/nel
olksdr Oct 13, 2023
f005272
Add snapshots
olksdr Oct 13, 2023
e89bc63
Move NEL into context
olksdr Oct 13, 2023
eabb20b
fix doc lint
olksdr Oct 13, 2023
a61d9a4
Docs and typos
olksdr Oct 16, 2023
f2cdee0
fix tests
olksdr Oct 16, 2023
bc3aafc
Merge remote-tracking branch 'origin/master' into hackweek/nel
olksdr Oct 17, 2023
282ff20
Address review comments
olksdr Oct 17, 2023
2ea8209
refactor repetitions in the code
olksdr Oct 17, 2023
ede374e
PR review. Fix typos
olksdr Oct 17, 2023
e860db5
Remove wrong doc comment
olksdr Oct 17, 2023
d9de7f9
Explicitly set the event id for the new item
olksdr Oct 17, 2023
8530410
Make sure that each item has differnet event id in the envelope
olksdr Oct 17, 2023
8b1de5a
set status code conditionaly, remove dbg
olksdr Oct 17, 2023
c5777db
Merge branch 'master' into hackweek/nel
olksdr Oct 18, 2023
5858d8c
Merge branch 'master' into hackweek/nel
olksdr Oct 19, 2023
ddae542
Update test "test_processing_quotas" to include "nel" quota checks
olksdr Oct 19, 2023
82b8932
Update relay-event-schema/src/protocol/contexts/nel.rs
oioki Oct 30, 2023
b1b3c68
Merge branch 'master' into hackweek/nel
olksdr Oct 31, 2023
13cc9e2
review comments
olksdr Oct 31, 2023
ecc9249
fix lint and tests
olksdr Oct 31, 2023
1d02da1
Merge branch 'master' into hackweek/nel
olksdr Oct 31, 2023
0c3773a
Move functionality to normalization crate
olksdr Oct 31, 2023
e6a4d27
Bump timeout
olksdr Oct 31, 2023
2d4aba3
Merge remote-tracking branch 'origin/master' into hackweek/nel
olksdr Oct 31, 2023
8bc4b23
Merge branch 'master' into hackweek/nel
olksdr Nov 6, 2023
61cd8c9
Use event timestamp when available for calculation
olksdr Nov 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

**Features**:

- Added support for NEL (Network Error Logging) reports ingestion. ([#2421](https://github.com/getsentry/relay/pull/2421))

## 23.10.0

**Features**:
Expand Down
1 change: 1 addition & 0 deletions py/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- Add `scraping_attempts` field to the event schema. ([#2575](https://github.com/getsentry/relay/pull/2575))
jjbayer marked this conversation as resolved.
Show resolved Hide resolved
- Added support for NEL (Network Error Logging) reports ingestion. ([#2421](https://github.com/getsentry/relay/pull/2421))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
- Added support for NEL (Network Error Logging) reports ingestion. ([#2421](https://github.com/getsentry/relay/pull/2421))
- Add context for NEL (Network Error Logging) reports to the event schema. ([#2421](https://github.com/getsentry/relay/pull/2421))


## 0.8.31

Expand Down
2 changes: 1 addition & 1 deletion relay-base-schema/src/data_category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ impl FromStr for DataCategory {
impl From<EventType> for DataCategory {
fn from(ty: EventType) -> Self {
match ty {
EventType::Default | EventType::Error => Self::Error,
EventType::Default | EventType::Error | EventType::Nel => Self::Error,
EventType::Transaction => Self::Transaction,
EventType::Csp | EventType::Hpkp | EventType::ExpectCt | EventType::ExpectStaple => {
Self::Security
Expand Down
4 changes: 4 additions & 0 deletions relay-base-schema/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pub enum EventType {
ExpectCt,
/// An ExpectStaple violation payload.
ExpectStaple,
/// Network Error Logging report.
Nel,
/// Performance monitoring transactions carrying spans.
Transaction,
/// All events that do not qualify as any other type.
Expand Down Expand Up @@ -70,6 +72,7 @@ impl FromStr for EventType {
"hpkp" => EventType::Hpkp,
"expectct" => EventType::ExpectCt,
"expectstaple" => EventType::ExpectStaple,
"nel" => EventType::Nel,
"transaction" => EventType::Transaction,
_ => return Err(ParseEventTypeError),
})
Expand All @@ -85,6 +88,7 @@ impl fmt::Display for EventType {
EventType::Hpkp => write!(f, "hpkp"),
EventType::ExpectCt => write!(f, "expectct"),
EventType::ExpectStaple => write!(f, "expectstaple"),
EventType::Nel => write!(f, "nel"),
EventType::Transaction => write!(f, "transaction"),
}
}
Expand Down
19 changes: 18 additions & 1 deletion relay-event-normalization/src/normalize/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use relay_event_schema::processor::{
use relay_event_schema::protocol::{
AsPair, Breadcrumb, ClientSdkInfo, Context, ContextInner, Contexts, DebugImage, DeviceClass,
Event, EventId, EventType, Exception, Frame, Headers, IpAddr, Level, LogEntry, Measurement,
Measurements, ReplayContext, Request, SpanAttribute, SpanStatus, Stacktrace, Tags,
Measurements, NelContext, ReplayContext, Request, SpanAttribute, SpanStatus, Stacktrace, Tags,
TraceContext, User, VALID_PLATFORMS,
};
use relay_protocol::{
Expand Down Expand Up @@ -264,6 +264,8 @@ impl<'a> NormalizeProcessor<'a> {
EventType::ExpectCt
} else if event.expectstaple.value().is_some() {
EventType::ExpectStaple
} else if event.context::<NelContext>().is_some() {
EventType::Nel
} else {
EventType::Default
}
Expand Down Expand Up @@ -675,6 +677,18 @@ fn is_security_report(event: &Event) -> bool {
|| event.hpkp.value().is_some()
}

/// Backfills the client IP address on for the NEL reports.
fn normalize_nel_report(event: &mut Event, client_ip: Option<&IpAddr>) {
if event.context::<NelContext>().is_none() {
return;
}

if let Some(client_ip) = client_ip {
let user = event.user.value_mut().get_or_insert_with(User::default);
user.ip_address = Annotated::new(client_ip.to_owned());
}
}

/// Backfills common security report attributes.
fn normalize_security_report(
event: &mut Event,
Expand Down Expand Up @@ -1024,6 +1038,9 @@ pub fn light_normalize_event(
// Process security reports first to ensure all props.
normalize_security_report(event, config.client_ip, &config.user_agent);

// Process NEL reports to ensure all props.
normalize_nel_report(event, config.client_ip);

// Insert IP addrs before recursing, since geo lookup depends on it.
normalize_ip_addresses(
&mut event.request,
Expand Down
4 changes: 4 additions & 0 deletions relay-event-schema/src/protocol/contexts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod cloud_resource;
mod device;
mod gpu;
mod monitor;
mod nel;
mod os;
mod otel;
mod profile;
Expand All @@ -19,6 +20,7 @@ pub use cloud_resource::*;
pub use device::*;
pub use gpu::*;
pub use monitor::*;
pub use nel::*;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here wrt star exports.

pub use os::*;
pub use otel::*;
pub use profile::*;
Expand Down Expand Up @@ -78,6 +80,8 @@ pub enum Context {
Otel(Box<OtelContext>),
/// Cloud resource information.
CloudResource(Box<CloudResourceContext>),
/// Nel information.
Nel(Box<NelContext>),
/// Additional arbitrary fields for forwards compatibility.
#[metastructure(fallback_variant)]
Other(#[metastructure(pii = "true")] Object<Value>),
Expand Down
61 changes: 61 additions & 0 deletions relay-event-schema/src/protocol/contexts/nel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#[cfg(feature = "jsonschema")]
use relay_jsonschema_derive::JsonSchema;
use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, Value};

use crate::processor::ProcessValue;

/// Contains the unique to NEL reports information.
///
/// NEL is a browser feature that allows reporting of failed network requests from the client side.
oioki marked this conversation as resolved.
Show resolved Hide resolved
/// W3C Editor's Draft: <https://w3c.github.io/network-error-logging/>
/// MDN: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Network_Error_Logging>
#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
#[cfg_attr(feature = "jsonschema", derive(JsonSchema))]
pub struct NelContext {
/// If request failed, the type of its network error. If request succeeded, "ok".
#[metastructure(field = "type")]
pub ty: Annotated<String>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: If we make type an enum with an other(String) variant, we can probably shave off a string allocation for most use cases:

The type of the network error may be one of the following pre-defined values from the specification, but browsers can add and send their own error types

https://developer.mozilla.org/en-US/docs/Web/HTTP/Network_Error_Logging#dns.unreachable

/// Server IP where the requests was sent to.
#[metastructure(pii = "true")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Should we rather mark that as pii = "maybe"? If that is the target server IP, it is still sensitive information but does not identify users.
  • Could we use the IpAddr type here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

pub server_ip: Annotated<String>,
/// The time between the start of the resource fetch and when it was completed or aborted.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please document, which unit this time is specified in?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

pub elapsed_time: Annotated<u64>,
/// If request failed, the phase of its network error. If request succeeded, "application".
pub phase: Annotated<String>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we introduce an enumeration for the known phases, with an Other(String) variant as fallback? According to the specification, the known phases are dns, connection, and application.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

/// The sampling rate.
pub sampling_fraction: Annotated<f64>,
/// For forward compatibility.
#[metastructure(additional_properties, pii = "maybe")]
pub other: Object<Value>,
}

impl super::DefaultContext for NelContext {
fn default_key() -> &'static str {
"nel"
}

fn from_context(context: super::Context) -> Option<Self> {
match context {
super::Context::Nel(c) => Some(*c),
_ => None,
}
}

fn cast(context: &super::Context) -> Option<&Self> {
match context {
super::Context::Nel(c) => Some(c),
_ => None,
}
}

fn cast_mut(context: &mut super::Context) -> Option<&mut Self> {
match context {
super::Context::Nel(c) => Some(c),
_ => None,
}
}

fn into_context(self) -> super::Context {
super::Context::Nel(Box::new(self))
}
}
2 changes: 2 additions & 0 deletions relay-event-schema/src/protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mod logentry;
mod measurements;
mod mechanism;
mod metrics;
mod nel;
mod relay_info;
mod replay;
mod request;
Expand Down Expand Up @@ -52,6 +53,7 @@ pub use self::logentry::*;
pub use self::measurements::*;
pub use self::mechanism::*;
pub use self::metrics::*;
pub use self::nel::*;
pub use self::relay_info::*;
pub use self::replay::*;
pub use self::request::*;
Expand Down
132 changes: 132 additions & 0 deletions relay-event-schema/src/protocol/nel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//! Contains definitions for the Network Error Logging (NEL) interface.
//!
//! NEL is a browser feature that allows reporting of failed network requests from the client side.
//! W3C Editor's Draft: <https://w3c.github.io/network-error-logging/>
//! MDN: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Network_Error_Logging>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider linking to the NEL context type for these docs, that way they don't have to be maintained in two places when we add more information to them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


#[cfg(feature = "jsonschema")]
use relay_jsonschema_derive::JsonSchema;
use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, Value};
use thiserror::Error;

/// The NEL parsing errors.
#[derive(Debug, Error)]
pub enum NetworkReportError {
/// Unexpected format.
#[error("unexpected format")]
InvalidNel,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the InvalidNel error variant is not instantiated anywhere, and the only other variant covers deserialization. Could we simplify this by calling Annotated::from_json_bytes(value).map_err(ProcessingError::InvalidJson)? directly in event_from_nel_item like the other deserializing functions do?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

/// Incoming Json is unparsable.
#[error("incoming json is unparsable")]
InvalidJson(#[from] serde_json::Error),
}

/// Generated network error report (NEL).
#[derive(Debug, Default, Clone, PartialEq, FromValue, IntoValue, Empty)]
#[cfg_attr(feature = "jsonschema", derive(JsonSchema))]
pub struct BodyRaw {
/// The time between the start of the resource fetch and when it was completed or aborted.
pub elapsed_time: Annotated<u64>,
/// HTTP method.
pub method: Annotated<String>,
/// If request failed, the phase of its network error. If request succeeded, "application".
pub phase: Annotated<String>,
/// The HTTP protocol and version.
pub protocol: Annotated<String>,
/// Request's referrer, as determined by the referrer policy associated with its client.
pub referrer: Annotated<String>,
/// The sampling rate.
pub sampling_fraction: Annotated<f64>,
/// The IP address of the server where the site is hosted.
pub server_ip: Annotated<String>,
/// HTTP status code.
pub status_code: Annotated<i64>,
/// If request failed, the type of its network error. If request succeeded, "ok".
#[metastructure(field = "type")]
pub ty: Annotated<String>,
/// For forward compatibility.
#[metastructure(additional_properties, pii = "maybe")]
pub other: Object<Value>,
}

/// Models the content of a NEL report.
///
/// NOTE: This is the structure used inside the Event (serialization is based on Annotated
/// infrastructure). We also use a version of this structure to deserialize from raw JSON
/// via serde.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still true?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 nice catch, removed .

///
/// See <https://w3c.github.io/network-error-logging/>
#[derive(Debug, Default, Clone, PartialEq, FromValue, IntoValue, Empty)]
#[cfg_attr(feature = "jsonschema", derive(JsonSchema))]
pub struct NetworkReportRaw {
/// The age of the report since it got collected and before it got sent.
pub age: Annotated<i64>,
/// The type of the report.
#[metastructure(field = "type")]
pub ty: Annotated<String>,
/// The URL of the document in which the error occurred.
#[metastructure(pii = "true")]
pub url: Annotated<String>,
/// The User-Agent HTTP header.
pub user_agent: Annotated<String>,
/// The body of the NEL report.
pub body: Annotated<BodyRaw>,
/// For forward compatibility.
#[metastructure(additional_properties, pii = "maybe")]
pub other: Object<Value>,
}

impl NetworkReportRaw {
pub fn try_annotated_from(value: &[u8]) -> Result<Annotated<Self>, NetworkReportError> {
Annotated::from_json_bytes(value).map_err(NetworkReportError::InvalidJson)
}
}

#[cfg(test)]
mod tests {
use relay_protocol::assert_annotated_snapshot;

use crate::protocol::NetworkReportRaw;

#[test]
fn test_nel_raw_basic() {
let json = r#"{
"age": 31042,
"body": {
"elapsed_time": 0,
"method": "GET",
"phase": "connection",
"protocol": "http/1.1",
"referrer": "",
"sampling_fraction": 1.0,
"server_ip": "127.0.0.1",
"status_code": 0,
"type": "tcp.refused"
},
"type": "network-error",
"url": "http://example.com/",
"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"
}"#;

let report = NetworkReportRaw::try_annotated_from(json.as_bytes()).unwrap();

assert_annotated_snapshot!(report, @r###"
{
"age": 31042,
"type": "network-error",
"url": "http://example.com/",
"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",
"body": {
"elapsed_time": 0,
"method": "GET",
"phase": "connection",
"protocol": "http/1.1",
"referrer": "",
"sampling_fraction": 1.0,
"server_ip": "127.0.0.1",
"status_code": 0,
"type": "tcp.refused"
}
}
"###);
}
}
4 changes: 4 additions & 0 deletions relay-event-schema/src/protocol/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,9 @@ pub struct Request {
/// HTTP request method.
pub method: Annotated<String>,

/// HTTP protocol.
pub protocol: Annotated<String>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we introduce an enum for the well-known protocols? Particularly, it would be nice to normalize the casing of the protocols, so that both https and HTTPS can be sent in, but they are always stored in the same notation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not find the list of the protocols to be used here. The doc is not really clear on this.
I can postpone this to later PRs


/// Request data in any format that makes sense.
///
/// SDKs should discard large and binary bodies by default. Can be given as a string or
Expand Down Expand Up @@ -684,6 +687,7 @@ mod tests {
let request = Annotated::new(Request {
url: Annotated::new("https://google.com/search".to_string()),
method: Annotated::new("GET".to_string()),
protocol: Annotated::empty(),
data: {
let mut map = Object::new();
map.insert("some".to_string(), Annotated::new(Value::I64(1)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Event {
request: Request {
url: ~,
method: ~,
protocol: ~,
data: Object(
{
"query": String(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Event {
request: Request {
url: ~,
method: ~,
protocol: ~,
data: Object(
{
"query": String(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Event {
request: Request {
url: ~,
method: ~,
protocol: ~,
data: Object(
{
"query": String(
Expand Down
Loading