Skip to content

Commit

Permalink
Implement Network Error Logging (#2421)
Browse files Browse the repository at this point in the history
The first part of the NEL ([Network Error
Logging](https://developer.mozilla.org/en-US/docs/Web/HTTP/Network_Error_Logging))
implementation.

* Adds a new `Nel` EventType
* Adds a new `Nel` protocol with minimal validation (i.e. only specific
fields are captured)
* Adds a new `/api/:project_id/nel/` endpoint for NEL reports ingestion
  * simple content type validation
* splits payload into separate envelopes (a single HTTP NEL request
could contain several independent reports)
* `user.ip_address` field is set to the IP address of the request (real
user's IP address)
* An event is enriched with browser information derived from the
request's `User-Agent` header

Related PRs:
getsentry/sentry#55135
getsentry/sentry-kafka-schemas#177

---------

Co-authored-by: Oleksandr Kylymnychenko <oleksandr@sentry.io>
Co-authored-by: Oleksandr <1931331+olksdr@users.noreply.github.com>
Co-authored-by: Jan Michael Auer <mail@jauer.org>
  • Loading branch information
4 people committed Nov 6, 2023
1 parent 71e819c commit de5e0fa
Show file tree
Hide file tree
Showing 25 changed files with 811 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- Filter outliers (>180s) for mobile measurements. ([#2649](https://github.com/getsentry/relay/pull/2649))
- Allow access to more context fields in dynamic sampling and metric extraction. ([#2607](https://github.com/getsentry/relay/pull/2607), [#2640](https://github.com/getsentry/relay/pull/2640), [#2675](https://github.com/getsentry/relay/pull/2675))
- Allow advanced scrubbing expressions for datascrubbing safe fields. ([#2670](https://github.com/getsentry/relay/pull/2670))
- Add context for NEL (Network Error Logging) reports to the event schema. ([#2421](https://github.com/getsentry/relay/pull/2421))

**Bug Fixes**:

Expand Down
5 changes: 5 additions & 0 deletions py/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

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

## 0.8.33

- Drop events starting or ending before January 1, 1970 UTC. ([#2613](https://github.com/getsentry/relay/pull/2613))
Expand All @@ -12,6 +16,7 @@
## 0.8.32

- Add `scraping_attempts` field to the event schema. ([#2575](https://github.com/getsentry/relay/pull/2575))
- Drop events starting or ending before January 1, 1970 UTC. ([#2613](https://github.com/getsentry/relay/pull/2613))

## 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 @@ -160,7 +160,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,
/// User feedback payload.
Expand Down Expand Up @@ -74,6 +76,7 @@ impl FromStr for EventType {
"hpkp" => EventType::Hpkp,
"expectct" => EventType::ExpectCt,
"expectstaple" => EventType::ExpectStaple,
"nel" => EventType::Nel,
"transaction" => EventType::Transaction,
"feedback" => EventType::UserReportV2,
_ => return Err(ParseEventTypeError),
Expand All @@ -90,6 +93,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"),
EventType::UserReportV2 => write!(f, "feedback"),
}
Expand Down
20 changes: 19 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 All @@ -36,6 +36,7 @@ use crate::{
};

pub mod breakdowns;
pub mod nel;
pub mod span;
pub mod user_agent;
pub mod utils;
Expand Down Expand Up @@ -270,6 +271,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 @@ -774,6 +777,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 @@ -1172,6 +1187,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
90 changes: 90 additions & 0 deletions relay-event-normalization/src/normalize/nel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//! Contains helper function for NEL reports.

use chrono::{Duration, Utc};
use relay_event_schema::protocol::{
Contexts, Event, HeaderName, HeaderValue, Headers, LogEntry, NelContext, NetworkReportRaw,
Request, ResponseContext, Timestamp,
};
use relay_protocol::Annotated;

/// Enriches the event with new values using the provided [`NetworkReportRaw`].
pub fn enrich_event(event: &mut Event, nel: Annotated<NetworkReportRaw>) {
// If the incoming NEL report is empty or it contains an empty body, just exit.
let Some(nel) = nel.into_value() else {
return;
};
let Some(body) = nel.body.into_value() else {
return;
};

event.logger = Annotated::from("nel".to_string());

event.logentry = Annotated::new(LogEntry::from({
if nel.ty.value().map_or("<unknown-type>", |v| v.as_str()) == "http.error" {
format!(
"{} / {} ({})",
body.phase.as_str().unwrap_or("<unknown-phase>"),
body.ty.as_str().unwrap_or("<unknown-type>"),
body.status_code.value().unwrap_or(&0)
)
} else {
format!(
"{} / {}",
body.phase.as_str().unwrap_or("<unknown-phase>"),
body.ty.as_str().unwrap_or("<unknown-type>"),
)
}
}));

let request = event.request.get_or_insert_with(Request::default);
request.url = nel.url;
request.method = body.method;
request.protocol = body.protocol;

let headers = request.headers.get_or_insert_with(Headers::default);

if let Some(ref user_agent) = nel.user_agent.value() {
if !user_agent.is_empty() {
headers.insert(
HeaderName::new("user-agent"),
HeaderValue::new(user_agent).into(),
);
}
}

if let Some(referrer) = body.referrer.value() {
headers.insert(
HeaderName::new("referer"),
HeaderValue::new(referrer).into(),
);
}

let contexts = event.contexts.get_or_insert_with(Contexts::new);

let nel_context = contexts.get_or_default::<NelContext>();
nel_context.server_ip = body.server_ip;
nel_context.elapsed_time = body.elapsed_time;
nel_context.error_type = body.ty;
nel_context.phase = body.phase;
nel_context.sampling_fraction = body.sampling_fraction;

// Set response status code only if it's bigger than zero.
let status_code = body
.status_code
.map_value(|v| u64::try_from(v).unwrap_or(0));
if status_code.value().unwrap_or(&0) > &0 {
let response_context = contexts.get_or_default::<ResponseContext>();
response_context.status_code = status_code;
}

// Set the timestamp on the event when it actually occurred.
let event_time = event
.timestamp
.value_mut()
.map_or(Utc::now(), |timestamp| timestamp.into_inner());
if let Some(event_time) =
event_time.checked_sub_signed(Duration::milliseconds(*nel.age.value().unwrap_or(&0)))
{
event.timestamp = Annotated::new(Timestamp::from(event_time))
}
}
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::*;
pub use os::*;
pub use otel::*;
pub use profile::*;
Expand Down Expand Up @@ -82,6 +84,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
63 changes: 63 additions & 0 deletions relay-event-schema/src/protocol/contexts/nel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#[cfg(feature = "jsonschema")]
use relay_jsonschema_derive::JsonSchema;
use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, Value};

use crate::processor::ProcessValue;
use crate::protocol::{IpAddr, NetworkReportPhases};

/// Contains NEL report information.
///
/// Network Error Logging (NEL) is a browser feature that allows reporting of failed network
/// requests from the client side. See the following resources for more information:
///
/// - [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".
pub error_type: Annotated<String>,
/// Server IP where the requests was sent to.
#[metastructure(pii = "maybe")]
pub server_ip: Annotated<IpAddr>,
/// The number of milliseconds between the start of the resource fetch and when it was aborted by the user agent.
pub elapsed_time: Annotated<u64>,
/// If request failed, the phase of its network error. If request succeeded, "application".
pub phase: Annotated<NetworkReportPhases>,
/// 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

0 comments on commit de5e0fa

Please sign in to comment.