Compare commits

...
Author SHA1 Message Date
Brandon Thomas aee4c653eb artcraft version header 2026-07-03 20:58:43 -04:00
Brandon Thomas 53c502e75c Add more fields to the pager alerting 2026-07-03 20:47:29 -04:00
7 changed files with 151 additions and 0 deletions
@@ -82,6 +82,7 @@ fn do_build_cors_config(is_production: bool) -> Cors {
actix_http::header::ACCESS_CONTROL_ALLOW_CREDENTIALS, // https://stackoverflow.com/a/46412839
actix_http::header::HeaderName::from_static("x-requested-with"), // Tabulator Ajax sends
actix_http::header::HeaderName::from_static("session"), // Custom header sent by Three.js Storyteller Studio
actix_http::header::HeaderName::from_static("x-artcraft-version"), // ArtCraft client app release, surfaced in error alerts
])
.max_age(3600)
}
@@ -63,6 +63,12 @@ mod tests {
media_file_token: None,
inference_job_token: None,
third_party_id: None,
http_host: None,
http_origin: None,
http_referer: None,
http_user_agent: None,
artcraft_version: None,
trace_id: None,
request_ip_address: None,
avt_cookie_token: None,
session_token: None,
@@ -228,6 +234,12 @@ mod tests {
media_file_token: None,
inference_job_token: None,
third_party_id: None,
http_host: None,
http_origin: None,
http_referer: None,
http_user_agent: None,
artcraft_version: None,
trace_id: None,
request_ip_address: None,
avt_cookie_token: None,
session_token: None,
@@ -37,6 +37,27 @@ pub struct NotificationDetails {
/// HTTP status code associated with the event, if any.
pub(crate) http_status_code: Option<u16>,
/// Hostname the client used to reach this server (from the `Host` or
/// forwarded-host headers), if any. Useful when a server is reachable
/// via multiple domains (eg. api.storyteller.ai, api.fakeyou.com).
pub(crate) http_host: Option<String>,
/// `Origin` header of the request, if any. For browser XHR/fetch this is
/// the scheme+host of the calling frontend.
pub(crate) http_origin: Option<String>,
/// `Referer` header of the request, if any. For browser XHR/fetch this is
/// the URL of the page that made the call (possibly truncated to the
/// origin by the browser's referrer policy).
pub(crate) http_referer: Option<String>,
/// `User-Agent` header of the request, if any.
pub(crate) http_user_agent: Option<String>,
/// `X-ArtCraft-Version` header of the request, if any. Sent by ArtCraft
/// clients to identify which app release made the call.
pub(crate) artcraft_version: Option<String>,
/// User token associated with the event, if any.
pub(crate) user_token: Option<String>,
@@ -188,6 +209,26 @@ impl NotificationDetails {
parts.push(format!("HTTP Status Code: {}", status_code));
}
if let Some(host) = &self.http_host {
parts.push(format!("HTTP Host: {}", host));
}
if let Some(origin) = &self.http_origin {
parts.push(format!("HTTP Origin: {}", origin));
}
if let Some(referer) = &self.http_referer {
parts.push(format!("HTTP Referer: {}", referer));
}
if let Some(user_agent) = &self.http_user_agent {
parts.push(format!("User Agent: {}", user_agent));
}
if let Some(version) = &self.artcraft_version {
parts.push(format!("ArtCraft Version: {}", version));
}
if let Some(ip) = &self.request_ip_address {
parts.push(format!("Request IP: {}", ip));
}
@@ -21,6 +21,11 @@ pub struct NotificationDetailsBuilder {
pub(crate) http_method: Option<String>,
pub(crate) http_path: Option<String>,
pub(crate) http_status_code: Option<u16>,
pub(crate) http_host: Option<String>,
pub(crate) http_origin: Option<String>,
pub(crate) http_referer: Option<String>,
pub(crate) http_user_agent: Option<String>,
pub(crate) artcraft_version: Option<String>,
pub(crate) user_token: Option<String>,
pub(crate) media_file_token: Option<String>,
@@ -53,6 +58,11 @@ impl NotificationDetailsBuilder {
http_method: None,
http_path: None,
http_status_code: None,
http_host: None,
http_origin: None,
http_referer: None,
http_user_agent: None,
artcraft_version: None,
user_token: None,
media_file_token: None,
inference_job_token: None,
@@ -84,6 +94,11 @@ impl NotificationDetailsBuilder {
http_method: None,
http_path: None,
http_status_code: None,
http_host: None,
http_origin: None,
http_referer: None,
http_user_agent: None,
artcraft_version: None,
user_token: None,
media_file_token: None,
inference_job_token: None,
@@ -138,6 +153,31 @@ impl NotificationDetailsBuilder {
self
}
pub fn set_http_host(mut self, http_host: Option<String>) -> Self {
self.http_host = http_host;
self
}
pub fn set_http_origin(mut self, http_origin: Option<String>) -> Self {
self.http_origin = http_origin;
self
}
pub fn set_http_referer(mut self, http_referer: Option<String>) -> Self {
self.http_referer = http_referer;
self
}
pub fn set_http_user_agent(mut self, http_user_agent: Option<String>) -> Self {
self.http_user_agent = http_user_agent;
self
}
pub fn set_artcraft_version(mut self, artcraft_version: Option<String>) -> Self {
self.artcraft_version = artcraft_version;
self
}
pub fn set_user_token(mut self, user_token: Option<String>) -> Self {
self.user_token = user_token;
self
@@ -202,6 +242,11 @@ impl NotificationDetailsBuilder {
http_method: self.http_method,
http_path: self.http_path,
http_status_code: self.http_status_code,
http_host: self.http_host,
http_origin: self.http_origin,
http_referer: self.http_referer,
http_user_agent: self.http_user_agent,
artcraft_version: self.artcraft_version,
user_token: self.user_token,
media_file_token: self.media_file_token,
inference_job_token: self.inference_job_token,
@@ -46,6 +46,11 @@ pub(crate) fn check_common_web_error(
.set_http_path(Some(path.to_string()))
.set_trace_id(metadata.trace_id.clone())
.set_http_status_code(Some(500))
.set_http_host(metadata.http_host.clone())
.set_http_origin(metadata.http_origin.clone())
.set_http_referer(metadata.http_referer.clone())
.set_http_user_agent(metadata.http_user_agent.clone())
.set_artcraft_version(metadata.artcraft_version.clone())
.set_request_ip_address(metadata.request_ip_address.clone())
.set_avt_cookie_token(metadata.avt_cookie_token.clone())
.set_session_token(metadata.session_token.clone())
@@ -32,6 +32,11 @@ pub(crate) fn check_status_code_fallback(
.set_http_path(Some(path.to_string()))
.set_trace_id(metadata.trace_id.clone())
.set_http_status_code(Some(status_code))
.set_http_host(metadata.http_host.clone())
.set_http_origin(metadata.http_origin.clone())
.set_http_referer(metadata.http_referer.clone())
.set_http_user_agent(metadata.http_user_agent.clone())
.set_artcraft_version(metadata.artcraft_version.clone())
.set_request_ip_address(metadata.request_ip_address.clone())
.set_avt_cookie_token(metadata.avt_cookie_token.clone())
.set_session_token(metadata.session_token.clone())
@@ -1,8 +1,10 @@
use std::sync::Arc;
use actix_web::dev::ServiceRequest;
use actix_web::http::header::{HeaderName, ORIGIN, REFERER, USER_AGENT};
use actix_web::web;
use actix_web::HttpMessage;
use actix_web::HttpRequest;
use http_server_common::request::get_request_ip::get_request_ip;
use log::debug;
@@ -10,6 +12,9 @@ use trace_id::TraceId;
use crate::state::server_state::ServerState;
/// Optional header sent by ArtCraft clients identifying the app release.
const ARTCRAFT_VERSION_HEADER: &str = "x-artcraft-version";
/// Optional per-request context extracted by the error alerting middleware
/// and forwarded to each handler-specific check. Every field fails open:
/// if extraction errors, the field is `None`.
@@ -21,6 +26,18 @@ pub(crate) struct RequestDebuggingMetadata {
pub session_user_token: Option<String>,
/// Set by `TraceIdMiddleware` (outermost middleware) on every request.
pub trace_id: Option<String>,
/// Hostname the client used to reach this server (Host / forwarded-host
/// headers). Distinguishes api.storyteller.ai vs api.fakeyou.com etc.
pub http_host: Option<String>,
/// `Origin` header — scheme+host of the frontend page making the XHR call.
pub http_origin: Option<String>,
/// `Referer` header — URL of the frontend page making the XHR call
/// (may be truncated to the origin by the browser's referrer policy).
pub http_referer: Option<String>,
/// `User-Agent` header.
pub http_user_agent: Option<String>,
/// `X-ArtCraft-Version` header — app release of the ArtCraft client, if sent.
pub artcraft_version: Option<String>,
}
impl RequestDebuggingMetadata {
@@ -62,12 +79,37 @@ impl RequestDebuggingMetadata {
.map(|payload| (Some(payload.session_token), payload.maybe_user_token))
.unwrap_or((None, None));
// `connection_info()` prefers Forwarded / X-Forwarded-Host over the raw
// Host header, so this reflects the hostname the client actually used
// even behind the load balancer.
let http_host = Some(http_request.connection_info().host().to_string())
.filter(|host| !host.is_empty());
let http_origin = get_header_value(http_request, ORIGIN);
let http_referer = get_header_value(http_request, REFERER);
let http_user_agent = get_header_value(http_request, USER_AGENT);
let artcraft_version =
get_header_value(http_request, HeaderName::from_static(ARTCRAFT_VERSION_HEADER));
Self {
request_ip_address,
avt_cookie_token,
session_token,
session_user_token,
trace_id,
http_host,
http_origin,
http_referer,
http_user_agent,
artcraft_version,
}
}
}
/// Read a header as a UTF-8 string; `None` if absent or not valid UTF-8.
fn get_header_value(request: &HttpRequest, header_name: HeaderName) -> Option<String> {
request.headers()
.get(header_name)
.and_then(|value| value.to_str().ok())
.map(|value| value.to_string())
}