relay_server/extractors/
request_meta.rs

1use std::convert::Infallible;
2use std::fmt;
3use std::net::{IpAddr, SocketAddr};
4use std::str::FromStr;
5
6use axum::RequestPartsExt;
7use axum::extract::rejection::PathRejection;
8use axum::extract::{ConnectInfo, FromRequestParts, OptionalFromRequestParts, Path};
9use axum::http::StatusCode;
10use axum::http::header::{self, AsHeaderName};
11use axum::http::request::Parts;
12use axum::response::{IntoResponse, Response};
13use chrono::{DateTime, Utc};
14use data_encoding::BASE64;
15use relay_auth::{RelayId, Signature};
16use relay_base_schema::organization::OrganizationId;
17use relay_base_schema::project::{ParseProjectKeyError, ProjectId, ProjectKey};
18use relay_common::{Auth, Dsn, ParseAuthError, ParseDsnError, Scheme};
19use relay_config::UpstreamDescriptor;
20use relay_event_normalization::{ClientHints, RawUserAgentInfo};
21use relay_quotas::Scoping;
22use serde::{Deserialize, Serialize};
23use url::Url;
24
25use crate::extractors::{ForwardedFor, ReceivedAt, SignatureError};
26use crate::service::ServiceState;
27use crate::statsd::{ClientName, RelayCounters};
28use crate::utils::ApiErrorResponse;
29
30#[derive(Debug, thiserror::Error)]
31pub enum BadEventMeta {
32    #[error("missing authorization information")]
33    MissingAuth,
34
35    #[error("multiple authorization payloads detected")]
36    MultipleAuth,
37
38    #[error("unsupported protocol version ({0})")]
39    UnsupportedProtocolVersion(u16),
40
41    #[error("bad envelope authentication header")]
42    BadEnvelopeAuth(#[source] serde_json::Error),
43
44    #[error("bad project path parameter")]
45    BadProject(#[from] PathRejection),
46
47    #[error("bad x-sentry-auth header")]
48    BadAuth(#[from] ParseAuthError),
49
50    #[error("bad sentry DSN public key")]
51    BadPublicKey(#[from] ParseProjectKeyError),
52
53    #[error("bad x-sentry-relay-signature header")]
54    SignatureError(SignatureError),
55}
56
57impl From<Infallible> for BadEventMeta {
58    fn from(infallible: Infallible) -> Self {
59        match infallible {}
60    }
61}
62
63impl IntoResponse for BadEventMeta {
64    fn into_response(self) -> Response {
65        let code = match self {
66            Self::MissingAuth
67            | Self::MultipleAuth
68            | Self::BadAuth(_)
69            | Self::BadEnvelopeAuth(_) => StatusCode::UNAUTHORIZED,
70            Self::UnsupportedProtocolVersion(_)
71            | Self::BadProject(_)
72            | Self::BadPublicKey(_)
73            | Self::SignatureError(_) => StatusCode::BAD_REQUEST,
74        };
75
76        (code, ApiErrorResponse::from_error(&self)).into_response()
77    }
78}
79
80/// Wrapper around a Sentry DSN with parsed public key.
81///
82/// The Sentry DSN type carries a plain public key string. However, Relay handles copy `ProjectKey`
83/// types internally. Converting from `String` to `ProjectKey` is fallible and should be caught when
84/// deserializing the request.
85///
86/// This type caches the parsed project key in addition to the DSN. Other than that, it
87/// transparently serializes to and deserializes from a DSN string.
88#[derive(Debug, Clone, Eq, PartialEq)]
89pub struct PartialDsn {
90    pub scheme: Scheme,
91    pub public_key: ProjectKey,
92    pub host: String,
93    pub port: u16,
94    pub path: String,
95    pub project_id: Option<ProjectId>,
96}
97
98impl PartialDsn {
99    /// Ensures a valid public key and project ID in the DSN.
100    fn from_dsn(dsn: Dsn) -> Result<Self, ParseDsnError> {
101        let project_id = dsn
102            .project_id()
103            .value()
104            .parse()
105            .map_err(|_| ParseDsnError::NoProjectId)?;
106
107        let public_key = dsn
108            .public_key()
109            .parse()
110            .map_err(|_| ParseDsnError::NoUsername)?;
111
112        Ok(Self {
113            scheme: dsn.scheme(),
114            public_key,
115            host: dsn.host().to_owned(),
116            port: dsn.port(),
117            path: dsn.path().to_owned(),
118            project_id: Some(project_id),
119        })
120    }
121
122    /// Creates a new [`PartialDsn`] for a Relay outbound request.
123    pub fn outbound(scoping: &Scoping, upstream: &UpstreamDescriptor<'_>) -> Self {
124        Self {
125            scheme: upstream.scheme(),
126            public_key: scoping.project_key,
127            host: upstream.host().to_owned(),
128            port: upstream.port(),
129            path: "".to_owned(),
130            project_id: Some(scoping.project_id),
131        }
132    }
133
134    /// Returns the project identifier that the DSN points to.
135    pub fn project_id(&self) -> Option<ProjectId> {
136        self.project_id
137    }
138
139    /// Returns the public key part of the DSN for authentication.
140    pub fn public_key(&self) -> ProjectKey {
141        self.public_key
142    }
143}
144
145impl fmt::Display for PartialDsn {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        write!(f, "{}://{}:@{}", self.scheme, self.public_key, self.host)?;
148        if self.port != self.scheme.default_port() {
149            write!(f, ":{}", self.port)?;
150        }
151        let project_id = self.project_id.unwrap_or_else(|| ProjectId::new(0));
152        write!(f, "/{}{}", self.path.trim_start_matches('/'), project_id)
153    }
154}
155
156impl FromStr for PartialDsn {
157    type Err = ParseDsnError;
158
159    fn from_str(s: &str) -> Result<Self, Self::Err> {
160        Self::from_dsn(Dsn::from_str(s)?)
161    }
162}
163
164impl<'de> Deserialize<'de> for PartialDsn {
165    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
166    where
167        D: serde::Deserializer<'de>,
168    {
169        let dsn = Dsn::deserialize(deserializer)?;
170        Self::from_dsn(dsn).map_err(serde::de::Error::custom)
171    }
172}
173
174impl Serialize for PartialDsn {
175    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
176    where
177        S: serde::Serializer,
178    {
179        serializer.collect_str(self)
180    }
181}
182
183const fn default_version() -> u16 {
184    relay_event_schema::protocol::PROTOCOL_VERSION
185}
186
187fn is_false(value: &bool) -> bool {
188    !*value
189}
190
191fn make_false() -> bool {
192    false
193}
194
195/// Request information for sentry ingest data, such as events, envelopes or metrics.
196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
197pub struct RequestMeta<D = PartialDsn> {
198    /// The DSN describing the target of this envelope.
199    dsn: D,
200
201    /// The client SDK that sent this event.
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    client: Option<String>,
204
205    /// The protocol version that the client speaks.
206    #[serde(default = "default_version")]
207    version: u16,
208
209    /// Value of the origin header in the incoming request, if present.
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    origin: Option<Url>,
212
213    /// IP address of the submitting remote.
214    #[serde(default, skip_serializing_if = "Option::is_none")]
215    remote_addr: Option<IpAddr>,
216
217    /// The full chain of request forward addresses, including the `remote_addr`.
218    #[serde(default, skip_serializing_if = "String::is_empty")]
219    forwarded_for: String,
220
221    /// The user agent that sent this event.
222    #[serde(default, skip_serializing_if = "Option::is_none")]
223    user_agent: Option<String>,
224
225    #[serde(default, skip_serializing_if = "ClientHints::is_empty")]
226    client_hints: ClientHints<String>,
227
228    /// A flag that indicates that project options caching should be bypassed.
229    #[serde(default = "make_false", skip_serializing_if = "is_false")]
230    no_cache: bool,
231
232    /// The time at which the request started.
233    ///
234    /// NOTE: This is internal-only and not exposed to Envelope headers.
235    #[serde(skip, default = "Utc::now")]
236    received_at: DateTime<Utc>,
237
238    /// Contains the signature information extracted from the request.
239    ///
240    /// This can be used, for example, to verify a trusted relay during ingestion.
241    ///
242    /// NOTE: This is internal only.
243    #[serde(skip)]
244    signature: Option<Signature>,
245
246    /// Whether the request is coming from an statically configured internal Relay.
247    ///
248    /// NOTE: This is internal-only and not exposed to Envelope headers.
249    #[serde(skip)]
250    from_internal_relay: bool,
251}
252
253impl<D> RequestMeta<D> {
254    /// Returns the client that sent this event (Sentry SDK identifier).
255    ///
256    /// The client is formatted as `"sdk/version"`, for example `"raven-node/2.6.3"`.
257    pub fn client(&self) -> Option<&str> {
258        self.client.as_deref()
259    }
260
261    /// Returns the name of the client that sent the event without version.
262    ///
263    /// If the client is not sent in standard format, this method returns `None`.
264    pub fn client_name(&self) -> ClientName {
265        self.client()
266            .and_then(|client| client.split_once('/'))
267            .map(|(client, _)| client)
268            .map_or(ClientName::Other("proprietary"), ClientName::from)
269    }
270
271    /// Returns the protocol version of the event payload.
272    #[allow(dead_code)] // used in tests and processing mode
273    pub fn version(&self) -> u16 {
274        self.version
275    }
276
277    /// Returns a reference to the origin URL
278    pub fn origin(&self) -> Option<&Url> {
279        self.origin.as_ref()
280    }
281
282    /// The IP address of the Relay or client that ingested the event.
283    #[allow(unused)]
284    pub fn remote_addr(&self) -> Option<IpAddr> {
285        self.remote_addr
286    }
287
288    /// The IP address of the client that this event originates from.
289    ///
290    /// This differs from `remote_addr` if the event was sent through a Relay or any other proxy
291    /// before.
292    pub fn client_addr(&self) -> Option<IpAddr> {
293        let client = self.forwarded_for().split(',').next()?;
294        client.trim().parse().ok()
295    }
296
297    /// Returns the value of the forwarded for header
298    pub fn forwarded_for(&self) -> &str {
299        &self.forwarded_for
300    }
301
302    /// The user agent that sent this event.
303    ///
304    /// This is the value of the `User-Agent` header. In contrast, `auth.client_agent()` identifies
305    /// the SDK that sent the event.
306    pub fn user_agent(&self) -> Option<&str> {
307        self.user_agent.as_deref()
308    }
309
310    pub fn client_hints(&self) -> &ClientHints<String> {
311        &self.client_hints
312    }
313
314    /// Indicates that caches should be bypassed.
315    pub fn no_cache(&self) -> bool {
316        self.no_cache
317    }
318
319    /// The time at which the request started.
320    pub fn received_at(&self) -> DateTime<Utc> {
321        self.received_at
322    }
323
324    /// Sets the received at for this [`RequestMeta`] on the current envelope.
325    pub fn set_received_at(&mut self, received_at: DateTime<Utc>) {
326        self.received_at = received_at
327    }
328
329    /// Whether the request is coming from a statically configured internal Relay.
330    pub fn is_from_internal_relay(&self) -> bool {
331        self.from_internal_relay
332    }
333
334    /// Overwrite internal property.
335    pub fn set_from_internal_relay(&mut self, value: bool) {
336        self.from_internal_relay = value;
337    }
338
339    /// Sets the client for this [`RequestMeta`] on the current envelope.
340    pub fn set_client(&mut self, client: String) {
341        self.client = Some(client);
342    }
343
344    /// Returns the trusted relay signature.
345    pub fn signature(&self) -> Option<&Signature> {
346        self.signature.as_ref()
347    }
348}
349
350impl RequestMeta {
351    /// Creates meta for an outbound request of this Relay.
352    pub fn outbound(dsn: PartialDsn) -> Self {
353        Self {
354            dsn,
355            client: Some(crate::constants::CLIENT.to_owned()),
356            version: default_version(),
357            origin: None,
358            remote_addr: None,
359            forwarded_for: "".to_owned(),
360            user_agent: Some(crate::constants::SERVER.to_owned()),
361            no_cache: false,
362            received_at: Utc::now(),
363            client_hints: ClientHints::default(),
364            signature: None,
365            from_internal_relay: false,
366        }
367    }
368
369    /// Returns a reference to the DSN.
370    ///
371    /// The DSN declares the project and auth information and upstream address. When RequestMeta is
372    /// constructed from a web request, the DSN is set to point to the upstream host.
373    pub fn dsn(&self) -> &PartialDsn {
374        &self.dsn
375    }
376
377    /// Returns the project identifier that the DSN points to.
378    ///
379    /// Returns `None` if the envelope was sent to the legacy `/api/store/` endpoint. In this case,
380    /// the DSN will be filled in during normalization. In all other cases, this will return
381    /// `Some(ProjectId)`.
382    pub fn project_id(&self) -> Option<ProjectId> {
383        self.dsn.project_id
384    }
385
386    /// Updates the DSN to the given project ID.
387    pub fn set_project_id(&mut self, project_id: ProjectId) {
388        self.dsn.project_id = Some(project_id);
389    }
390
391    /// Returns the public key part of the DSN for authentication.
392    pub fn public_key(&self) -> ProjectKey {
393        self.dsn.public_key
394    }
395
396    /// Formats the Sentry authentication header.
397    ///
398    /// This header must be included in store requests.
399    pub fn auth_header(&self) -> String {
400        let mut auth = format!(
401            "Sentry sentry_key={}, sentry_version={}",
402            self.public_key(),
403            self.version
404        );
405
406        if let Some(ref client) = self.client {
407            use std::fmt::Write;
408            write!(auth, ", sentry_client={client}").ok();
409        }
410
411        auth
412    }
413
414    /// Returns scoping information from the request.
415    ///
416    /// The scoping returned from this function is not complete since it lacks info from the Project
417    /// state. To fetch full scoping information, invoke the `GetScoping` message on `Project`.
418    pub fn get_partial_scoping(&self) -> Scoping {
419        Scoping {
420            organization_id: OrganizationId::new(0),
421            project_id: self.project_id().unwrap_or_else(|| ProjectId::new(0)),
422            project_key: self.public_key(),
423            key_id: None,
424        }
425    }
426}
427
428/// Request information without required authentication parts.
429///
430/// This is identical to [`RequestMeta`] with the exception that the DSN, used to authenticate, is
431/// optional.
432pub type PartialMeta = RequestMeta<Option<PartialDsn>>;
433
434impl PartialMeta {
435    /// Returns a reference to the DSN.
436    ///
437    /// The DSN declares the project and auth information and upstream address. When RequestMeta is
438    /// constructed from a web request, the DSN is set to point to the upstream host.
439    pub fn dsn(&self) -> Option<&PartialDsn> {
440        self.dsn.as_ref()
441    }
442
443    /// Completes missing information with complete `RequestMeta`.
444    ///
445    /// All fields that set in this instance will remain.
446    pub fn copy_to(self, mut complete: RequestMeta) -> RequestMeta {
447        // DSN needs to be validated by the caller and will not be copied over.
448
449        if self.client.is_some() {
450            complete.client = self.client;
451        }
452        if self.version != default_version() {
453            complete.version = self.version;
454        }
455        if self.origin.is_some() {
456            complete.origin = self.origin;
457        }
458        if self.remote_addr.is_some() {
459            complete.remote_addr = self.remote_addr;
460        }
461        if !self.forwarded_for.is_empty() {
462            complete.forwarded_for = self.forwarded_for;
463        }
464        if self.user_agent.is_some() {
465            complete.user_agent = self.user_agent;
466        }
467        if self.from_internal_relay {
468            complete.from_internal_relay = self.from_internal_relay;
469        }
470
471        complete.client_hints.copy_from(self.client_hints);
472
473        if self.no_cache {
474            complete.no_cache = true;
475        }
476
477        complete
478    }
479}
480
481impl FromRequestParts<ServiceState> for PartialMeta {
482    type Rejection = BadEventMeta;
483
484    async fn from_request_parts(
485        parts: &mut Parts,
486        state: &ServiceState,
487    ) -> Result<Self, Self::Rejection> {
488        let mut ua = RawUserAgentInfo::default();
489        for (key, value) in &parts.headers {
490            ua.set_ua_field_from_header(key.as_str(), value.to_str().ok().map(str::to_string));
491        }
492
493        let mut from_internal_relay = false;
494        let relay_id = parts
495            .headers
496            .get("x-sentry-relay-id")
497            .and_then(|h| h.to_str().ok())
498            .and_then(|h| h.parse::<RelayId>().ok());
499
500        if let Some(relay_id) = relay_id {
501            relay_log::configure_scope(|s| s.set_tag("relay_id", relay_id));
502            from_internal_relay = state
503                .config()
504                .static_relays()
505                .get(&relay_id)
506                .is_some_and(|ri| ri.internal);
507        }
508
509        let ReceivedAt(received_at) = ReceivedAt::from_request_parts(parts, state).await?;
510
511        let signature = Signature::from_request_parts(parts, state)
512            .await
513            .map_err(BadEventMeta::SignatureError)?;
514
515        Ok(RequestMeta {
516            dsn: None,
517            version: default_version(),
518            client: None,
519            origin: parse_header_url(parts, header::ORIGIN)
520                .or_else(|| parse_header_url(parts, header::REFERER)),
521            remote_addr: ConnectInfo::<SocketAddr>::from_request_parts(parts, state)
522                .await
523                .map(|ConnectInfo(peer)| peer.ip())
524                .ok(),
525            forwarded_for: ForwardedFor::from_request_parts(parts, state)
526                .await?
527                .into_inner(),
528            user_agent: ua.user_agent,
529            no_cache: false,
530            received_at,
531            client_hints: ua.client_hints,
532            signature,
533            from_internal_relay,
534        })
535    }
536}
537
538fn get_auth_header(req: &Parts, header_name: impl AsHeaderName) -> Option<&str> {
539    req.headers
540        .get(header_name)
541        .and_then(|x| x.to_str().ok())
542        .filter(|h| h.len() >= 7 && h[..7].eq_ignore_ascii_case("sentry "))
543}
544
545fn auth_from_parts(req: &Parts, path_key: Option<String>) -> Result<Auth, BadEventMeta> {
546    let mut auth = None;
547
548    // try to extract authentication info from http header "x-sentry-auth"
549    if let Some(header) = get_auth_header(req, "x-sentry-auth") {
550        auth = Some(header.parse::<Auth>()?);
551    }
552
553    // try to extract authentication info from http header "authorization"
554    if let Some(header) = get_auth_header(req, header::AUTHORIZATION) {
555        if auth.is_some() {
556            return Err(BadEventMeta::MultipleAuth);
557        }
558
559        auth = Some(header.parse::<Auth>()?);
560    }
561
562    // try to get authentication info from basic auth
563    if let Some(basic_auth) = req
564        .headers
565        .get("authorization")
566        .and_then(|value| value.to_str().ok())
567        .and_then(|x| {
568            if x.len() >= 6 && x[..6].eq_ignore_ascii_case("basic ") {
569                x.get(6..)
570            } else {
571                None
572            }
573        })
574        .and_then(|value| {
575            let decoded = String::from_utf8(BASE64.decode(value.as_bytes()).ok()?).ok()?;
576            let (public_key, _) = decoded.split_once(':')?;
577            Auth::from_pairs([("sentry_key", public_key)]).ok()
578        })
579    {
580        if auth.is_some() {
581            return Err(BadEventMeta::MultipleAuth);
582        }
583        auth = Some(basic_auth);
584    }
585
586    // try to extract authentication info from URL query_param .../?sentry_...=<key>...
587    let query = req.uri.query().unwrap_or_default();
588    if query.contains("sentry_") {
589        if auth.is_some() {
590            return Err(BadEventMeta::MultipleAuth);
591        }
592
593        auth = Some(Auth::from_querystring(query.as_bytes())?);
594    }
595
596    // try to extract authentication info from URL path segment .../{sentry_key}/...
597    if let Some(sentry_key) = path_key {
598        if auth.is_some() {
599            return Err(BadEventMeta::MultipleAuth);
600        }
601
602        auth = Some(
603            Auth::from_pairs(std::iter::once(("sentry_key", sentry_key)))
604                .map_err(|_| BadEventMeta::MissingAuth)?,
605        );
606    }
607
608    auth.ok_or(BadEventMeta::MissingAuth)
609}
610
611fn parse_header_url(req: &Parts, header: impl AsHeaderName) -> Option<Url> {
612    req.headers
613        .get(header)
614        .and_then(|h| h.to_str().ok())
615        .and_then(|s| s.parse::<Url>().ok())
616        .and_then(|u| match u.scheme() {
617            "http" | "https" => Some(u),
618            _ => None,
619        })
620}
621
622/// Path parameters containing authentication information for store endpoints.
623///
624/// These parameters implement part of the authentication mechanism. For more information, see
625/// [`RequestMeta`].
626#[derive(Debug, serde::Deserialize)]
627struct StorePath {
628    /// The numeric identifier of the Sentry project.
629    ///
630    /// This parameter is part of the store endpoint paths, which are generally located under
631    /// `/api/:project_id/*`. By default, all store endpoints have the project ID in the path. To
632    /// resolve the project and associated information, Relay actually uses the DSN's
633    /// [`ProjectKey`]. During ingestion, the stated project ID from the URI path is validated
634    /// against information resolved from the upstream.
635    ///
636    /// The legacy endpoint (`/api/store/`) does not have the project ID. In this case, Relay skips
637    /// ID validation during ingestion.
638    project_id: Option<ProjectId>,
639
640    /// The DSN's public key, also referred to as project key.
641    ///
642    /// Some endpoints require this key in the path. On all other endpoints, the key is either sent
643    /// as header or query parameter.
644    sentry_key: Option<String>,
645}
646
647impl FromRequestParts<ServiceState> for RequestMeta {
648    type Rejection = BadEventMeta;
649
650    async fn from_request_parts(
651        parts: &mut Parts,
652        state: &ServiceState,
653    ) -> Result<Self, Self::Rejection> {
654        let Path(store_path): Path<StorePath> =
655            parts.extract().await.map_err(BadEventMeta::BadProject)?;
656
657        let auth = auth_from_parts(parts, store_path.sentry_key)?;
658        let partial_meta: PartialMeta = parts.extract_with_state(state).await?;
659        let (public_key, key_flags) = ProjectKey::parse_with_flags(auth.public_key())?;
660
661        let config = state.config();
662        let upstream = config.upstream_descriptor();
663
664        let dsn = PartialDsn {
665            scheme: upstream.scheme(),
666            public_key,
667            host: upstream.host().to_owned(),
668            port: upstream.port(),
669            path: String::new(),
670            project_id: store_path.project_id,
671        };
672
673        // For now, we only handle <= v8 and drop everything else
674        let version = auth.version();
675        if version > relay_event_schema::protocol::PROTOCOL_VERSION {
676            return Err(BadEventMeta::UnsupportedProtocolVersion(version));
677        }
678
679        relay_statsd::metric!(
680            counter(RelayCounters::EventProtocol) += 1,
681            version = &version.to_string()
682        );
683
684        Ok(RequestMeta {
685            dsn,
686            version,
687            client: auth.client_agent().map(str::to_owned),
688            origin: partial_meta.origin,
689            remote_addr: partial_meta.remote_addr,
690            forwarded_for: partial_meta.forwarded_for,
691            user_agent: partial_meta.user_agent,
692            no_cache: key_flags.contains(&"no-cache"),
693            received_at: partial_meta.received_at,
694            client_hints: partial_meta.client_hints,
695            signature: partial_meta.signature,
696            from_internal_relay: partial_meta.from_internal_relay,
697        })
698    }
699}
700
701#[cfg(test)]
702mod tests {
703    use super::*;
704
705    impl RequestMeta {
706        // TODO: Remove Dsn here?
707        pub fn new(dsn: relay_common::Dsn) -> Self {
708            Self {
709                dsn: PartialDsn::from_dsn(dsn).expect("invalid DSN"),
710                client: Some("sentry/client".to_owned()),
711                version: 7,
712                origin: Some("http://origin/".parse().unwrap()),
713                remote_addr: Some("192.168.0.1".parse().unwrap()),
714                forwarded_for: String::new(),
715                user_agent: Some("sentry/agent".to_owned()),
716                no_cache: false,
717                received_at: Utc::now(),
718                client_hints: ClientHints::default(),
719                from_internal_relay: false,
720                signature: None,
721            }
722        }
723    }
724
725    #[test]
726    fn test_request_meta_roundtrip() {
727        let json = r#"{
728            "dsn": "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42",
729            "client": "sentry-client",
730            "version": 7,
731            "origin": "http://origin/",
732            "remote_addr": "192.168.0.1",
733            "forwarded_for": "8.8.8.8",
734            "user_agent": "0x8000",
735            "no_cache": false,
736            "client_hints":  {
737            "sec_ch_ua_platform": "macOS",
738            "sec_ch_ua_platform_version": "13.1.0",
739            "sec_ch_ua": "\"Not_A Brand\";v=\"99\", \"Google Chrome\";v=\"109\", \"Chromium\";v=\"109\""
740            }
741        }"#;
742
743        let mut deserialized: RequestMeta = serde_json::from_str(json).unwrap();
744
745        let reqmeta = RequestMeta {
746            dsn: PartialDsn::from_str("https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42")
747                .unwrap(),
748            client: Some("sentry-client".to_owned()),
749            version: 7,
750            origin: Some(Url::parse("http://origin/").unwrap()),
751            remote_addr: Some(IpAddr::from_str("192.168.0.1").unwrap()),
752            forwarded_for: "8.8.8.8".to_owned(),
753            user_agent: Some("0x8000".to_owned()),
754            no_cache: false,
755            received_at: Utc::now(),
756            client_hints: ClientHints {
757                sec_ch_ua_platform: Some("macOS".to_owned()),
758                sec_ch_ua_platform_version: Some("13.1.0".to_owned()),
759                sec_ch_ua: Some(
760                    "\"Not_A Brand\";v=\"99\", \"Google Chrome\";v=\"109\", \"Chromium\";v=\"109\""
761                        .to_owned(),
762                ),
763                sec_ch_ua_model: None,
764            },
765            from_internal_relay: false,
766            signature: None,
767        };
768        deserialized.received_at = reqmeta.received_at;
769        assert_eq!(deserialized, reqmeta);
770    }
771
772    #[test]
773    fn test_signature_not_serialized() {
774        let dsn: relay_common::Dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
775            .parse()
776            .unwrap();
777        let without_signature = RequestMeta::new(dsn.clone());
778        let mut with_signature = RequestMeta::new(dsn);
779        with_signature.signature = Some(Signature("test-signature".to_owned()));
780
781        let serialized_without_signature = serde_json::to_string(&without_signature).unwrap();
782        let serialized_with_signature = serde_json::to_string(&with_signature).unwrap();
783        assert_eq!(serialized_with_signature, serialized_without_signature);
784    }
785}