From 364d70b977466fbec80506455b1ea5e23f710480 Mon Sep 17 00:00:00 2001 From: Denis Cornehl Date: Sun, 10 Mar 2024 08:22:15 +0100 Subject: [PATCH 1/2] add sentry frontend error reporting & performance monitoring --- src/bin/cratesfyi.rs | 22 +++++++++++++--------- src/config.rs | 29 ++++++++++++++++++++++++++++- src/web/csp.rs | 12 +++++++++--- src/web/page/web_page.rs | 33 ++++++++++++++++++--------------- templates/base.html | 23 ++++++++++++++++++++++- 5 files changed, 90 insertions(+), 29 deletions(-) diff --git a/src/bin/cratesfyi.rs b/src/bin/cratesfyi.rs index b1e2a6e33..1b6803059 100644 --- a/src/bin/cratesfyi.rs +++ b/src/bin/cratesfyi.rs @@ -41,7 +41,16 @@ fn main() { .from_env_lossy(), ); - let _sentry_guard = if let Ok(sentry_dsn) = env::var("SENTRY_DSN") { + let ctx = BinContext::new(); + let config = match ctx.config().context("could not load config") { + Ok(config) => config, + Err(err) => { + eprintln!("{:?}", err); + std::process::exit(1); + } + }; + + let _sentry_guard = if let Some(ref sentry_dsn) = config.sentry_dsn { tracing::subscriber::set_global_default(tracing_registry.with( sentry_tracing::layer().event_filter(|md| { if md.fields().field("reported_to_sentry").is_some() { @@ -58,10 +67,7 @@ fn main() { sentry::ClientOptions { release: Some(docs_rs::BUILD_VERSION.into()), attach_stacktrace: true, - traces_sample_rate: env::var("SENTRY_TRACES_SAMPLE_RATE") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(0.0), + traces_sample_rate: config.sentry_traces_sample_rate.unwrap_or(0.0), ..Default::default() } .add_integration(sentry_panic::PanicIntegration::default()), @@ -71,7 +77,7 @@ fn main() { None }; - if let Err(err) = CommandLine::parse().handle_args() { + if let Err(err) = CommandLine::parse().handle_args(ctx) { let mut msg = format!("Error: {err}"); for cause in err.chain() { write!(msg, "\n\nCaused by:\n {cause}").unwrap(); @@ -156,9 +162,7 @@ enum CommandLine { } impl CommandLine { - fn handle_args(self) -> Result<()> { - let ctx = BinContext::new(); - + fn handle_args(self, ctx: BinContext) -> Result<()> { match self { Self::Build { subcommand } => subcommand.handle_args(ctx)?, Self::StartRegistryWatcher { diff --git a/src/config.rs b/src/config.rs index a4acf0aff..fd635dad1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,7 @@ use crate::{cdn::CdnKind, storage::StorageKind}; use anyhow::{anyhow, bail, Context, Result}; -use std::{env::VarError, error::Error, path::PathBuf, str::FromStr, time::Duration}; +use serde::ser::{Serialize, SerializeStruct, Serializer}; +use std::{env::VarError, error::Error, path::PathBuf, str::FromStr, sync::Arc, time::Duration}; use tracing::trace; use url::Url; @@ -11,6 +12,10 @@ pub struct Config { pub registry_url: Option, pub registry_api_host: Url, + // sentry config + pub sentry_dsn: Option, + pub sentry_traces_sample_rate: Option, + // Database connection params pub(crate) database_url: String, pub(crate) max_legacy_pool_size: u32, @@ -148,6 +153,9 @@ impl Config { )?, prefix: prefix.clone(), + sentry_dsn: maybe_env("SENTRY_DSN")?, + sentry_traces_sample_rate: maybe_env("SENTRY_TRACES_SAMPLE_RATE")?, + database_url: require_env("DOCSRS_DATABASE_URL")?, max_legacy_pool_size: env("DOCSRS_MAX_LEGACY_POOL_SIZE", 45)?, max_pool_size: env("DOCSRS_MAX_POOL_SIZE", 45)?, @@ -224,6 +232,25 @@ impl Config { } } +/// A more public version of the config that will be automaticaly exposed to the +/// Tera context. +pub(crate) struct PublicConfig(pub Arc); + +impl Serialize for PublicConfig { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("PublicConfig", 2)?; + state.serialize_field("sentry_dsn", &self.0.sentry_dsn)?; + state.serialize_field( + "sentry_traces_sample_rate", + &self.0.sentry_traces_sample_rate, + )?; + state.end() + } +} + fn env(var: &str, default: T) -> Result where T: FromStr, diff --git a/src/web/csp.rs b/src/web/csp.rs index 44608df4a..e1881907b 100644 --- a/src/web/csp.rs +++ b/src/web/csp.rs @@ -73,7 +73,7 @@ impl Csp { result.push_str("; font-src 'self'"); // Allow XHR. - result.push_str("; connect-src 'self'"); + result.push_str("; connect-src 'self' *.sentry.io"); // Only allow scripts with the random nonce attached to them. // @@ -83,6 +83,7 @@ impl Csp { // // This `.unwrap` is safe since the `Write` impl on str can never fail. write!(result, "; script-src 'nonce-{}'", self.nonce).unwrap(); + result.push_str(" https://browser.sentry-cdn.com https://js.sentry-cdn.com"); } fn render_svg(&self, result: &mut String) { @@ -192,8 +193,13 @@ mod tests { let csp = Csp::new(); assert_eq!( Some(format!( - "default-src 'none'; base-uri 'none'; img-src 'self' https:; \ - style-src 'self'; font-src 'self'; connect-src 'self'; script-src 'nonce-{}'", + "default-src 'none'; \ + base-uri 'none'; \ + img-src 'self' https:; \ + style-src 'self'; \ + font-src 'self'; \ + connect-src 'self' *.sentry.io; \ + script-src 'nonce-{}' https://browser.sentry-cdn.com https://js.sentry-cdn.com", csp.nonce() )), csp.render(ContentType::Html) diff --git a/src/web/page/web_page.rs b/src/web/page/web_page.rs index 7ead62b42..384b6c978 100644 --- a/src/web/page/web_page.rs +++ b/src/web/page/web_page.rs @@ -1,11 +1,16 @@ use super::TemplateData; -use crate::web::{csp::Csp, error::AxumNope}; +use crate::{ + config::PublicConfig, + web::{csp::Csp, error::AxumNope}, + Config, +}; use anyhow::Error; use axum::{ body::Body, extract::Request as AxumRequest, middleware::Next, response::{IntoResponse, Response as AxumResponse}, + Extension, }; use futures_util::future::{BoxFuture, FutureExt}; use http::header::CONTENT_LENGTH; @@ -123,6 +128,7 @@ pub(crate) struct DelayedTemplateRender { fn render_response( mut response: AxumResponse, templates: Arc, + config: Arc, csp_nonce: String, ) -> BoxFuture<'static, AxumResponse> { async move { @@ -133,6 +139,7 @@ fn render_response( cpu_intensive_rendering, } = render; context.insert("csp_nonce", &csp_nonce); + context.insert("config", &PublicConfig(config.clone())); let rendered = if cpu_intensive_rendering { templates @@ -160,6 +167,7 @@ fn render_response( return render_response( AxumNope::InternalError(err).into_response(), templates, + config, csp_nonce, ) .await; @@ -179,21 +187,16 @@ fn render_response( .boxed() } -pub(crate) async fn render_templates_middleware(req: AxumRequest, next: Next) -> AxumResponse { - let templates: Arc = req - .extensions() - .get::>() - .expect("template data request extension not found") - .clone(); - - let csp_nonce = req - .extensions() - .get::>() - .expect("csp request extension not found") - .nonce() - .to_owned(); +pub(crate) async fn render_templates_middleware( + Extension(config): Extension>, + Extension(templates): Extension>, + Extension(csp): Extension>, + req: AxumRequest, + next: Next, +) -> AxumResponse { + let csp_nonce = csp.nonce().to_owned(); let response = next.run(req).await; - render_response(response, templates, csp_nonce).await + render_response(response, templates, config, csp_nonce).await } diff --git a/templates/base.html b/templates/base.html index 6f3fb9ba8..d44d49c9e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -17,6 +17,27 @@ {%- block title -%} Docs.rs {%- endblock title -%} + {% if config.sentry_dsn %} + + + + {% endif %} + {%- block css -%}{%- endblock css -%} @@ -26,7 +47,7 @@ {%- block topbar -%} - {%- include "header/topbar.html" -%} + {%- include "header/topbar.html" -%} {%- endblock topbar -%} {%- block header %}{% endblock header -%} From 05001054ab2f5718041eba522fe512d0a2512f6f Mon Sep 17 00:00:00 2001 From: Denis Cornehl Date: Sun, 10 Mar 2024 12:07:20 +0100 Subject: [PATCH 2/2] switch to vendored sentry JS SDK --- src/web/csp.rs | 3 +-- templates/base.html | 8 ++++---- vendor/sentry/LICENSE | 21 +++++++++++++++++++++ vendor/sentry/bundle.tracing.7.105.0.min.js | 3 +++ 4 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 vendor/sentry/LICENSE create mode 100644 vendor/sentry/bundle.tracing.7.105.0.min.js diff --git a/src/web/csp.rs b/src/web/csp.rs index e1881907b..a08fbedaa 100644 --- a/src/web/csp.rs +++ b/src/web/csp.rs @@ -83,7 +83,6 @@ impl Csp { // // This `.unwrap` is safe since the `Write` impl on str can never fail. write!(result, "; script-src 'nonce-{}'", self.nonce).unwrap(); - result.push_str(" https://browser.sentry-cdn.com https://js.sentry-cdn.com"); } fn render_svg(&self, result: &mut String) { @@ -199,7 +198,7 @@ mod tests { style-src 'self'; \ font-src 'self'; \ connect-src 'self' *.sentry.io; \ - script-src 'nonce-{}' https://browser.sentry-cdn.com https://js.sentry-cdn.com", + script-src 'nonce-{}'", csp.nonce() )), csp.render(ContentType::Html) diff --git a/templates/base.html b/templates/base.html index d44d49c9e..b15a5a818 100644 --- a/templates/base.html +++ b/templates/base.html @@ -19,10 +19,10 @@ {% if config.sentry_dsn %} + nonce="{{ csp_nonce }}" + src="/-/static/sentry/bundle.tracing.7.105.0.min.js" + type="text/javascript"> +