This commit is contained in:
Andras Schmelczer 2026-05-13 12:11:54 +01:00
parent a08b5d2ae0
commit b98f0e3904
38 changed files with 3732 additions and 483 deletions

1
server-rs/Cargo.lock generated
View file

@ -3625,6 +3625,7 @@ dependencies = [
"tracing",
"tracing-appender",
"tracing-subscriber",
"url",
"urlencoding",
]

View file

@ -24,6 +24,7 @@ metrics = "0.24"
metrics-exporter-prometheus = "0.18"
reqwest = { version = "0.13", features = ["rustls", "json", "stream", "form"] }
urlencoding = "2"
url = "2"
rust_xlsxwriter = "0.94"
pmtiles = { version = "0.23", features = ["mmap-async-tokio"] }
rand = "0.10"

View file

@ -846,6 +846,8 @@ const MAX_RETRIES: usize = 3;
const MAX_REFINEMENTS: u32 = 3;
const MAX_TOTAL_ROUNDS: usize = 10;
const MAX_AI_QUERY_CHARS: usize = 5000;
pub async fn post_ai_filters(
State(shared): State<Arc<SharedState>>,
Extension(user): Extension<OptionalUser>,
@ -857,6 +859,14 @@ pub async fn post_ai_filters(
.0
.ok_or((StatusCode::UNAUTHORIZED, "Login required".into()))?;
if req.query.chars().count() > MAX_AI_QUERY_CHARS {
counter!("ai_requests_total", "status" => "query_too_long").increment(1);
return Err((
StatusCode::PAYLOAD_TOO_LARGE,
format!("Query too long (max {MAX_AI_QUERY_CHARS} chars)"),
));
}
// Check weekly token usage
let current_week = current_week_number();
let (stored_tokens, stored_week) = fetch_ai_usage(&state, &user.id).await?;

View file

@ -1,7 +1,8 @@
use std::collections::{HashMap, HashSet};
use metrics::counter;
use rustc_hash::FxHashMap;
use tracing::warn;
use tracing::error;
use crate::consts::MAX_PRICE_HISTORY_POINTS;
use crate::data::{FeatureStats, PostcodePoiMetrics, PropertyData};
@ -133,15 +134,21 @@ pub fn compute_feature_stats(
FeatureAccum::Enum { value_counts } => {
let value = data.get_feature(row, fi);
if value.is_finite() {
let idx = value as usize;
if idx < value_counts.len() {
value_counts[idx] += 1;
// Reject negatives, NaN-via-large-cast, and any out-of-range
// index. A schema/data mismatch is a critical data-integrity
// bug — skip the row, count it, and surface as error so
// monitoring catches it.
let len = value_counts.len();
let idx_ok = value >= 0.0 && (value as usize) < len;
if idx_ok {
value_counts[value as usize] += 1;
} else {
warn!(
counter!("stats_enum_oob_total").increment(1);
error!(
feature = feature_names[fi].as_str(),
idx,
max = value_counts.len(),
"Enum index out of bounds — possible data/schema mismatch"
value,
max = len,
"Enum index out of bounds — data/schema mismatch"
);
}
}

View file

@ -41,7 +41,8 @@ pub async fn get_streetview(
let resp = match state.http_client.get(&url).send().await {
Ok(r) => r,
Err(e) => {
warn!("Street View metadata request failed: {e}");
// Strip URL (contains the API key) from the error before logging.
warn!("Street View metadata request failed: {}", e.without_url());
return (
StatusCode::BAD_GATEWAY,
Json(StreetViewResponse {
@ -55,7 +56,7 @@ pub async fn get_streetview(
let meta: GoogleMetadataResponse = match resp.json().await {
Ok(m) => m,
Err(e) => {
warn!("Failed to parse Street View metadata: {e}");
warn!("Failed to parse Street View metadata: {}", e.without_url());
return (
StatusCode::BAD_GATEWAY,
Json(StreetViewResponse {

View file

@ -41,7 +41,7 @@ pub async fn post_telemetry(
// Entrypoint tracking (sent once per session)
if let Some(path) = &payload.entry_path {
let referrer = payload.referrer.as_deref().unwrap_or("direct");
let referrer = normalize_referrer_label(payload.referrer.as_deref().unwrap_or("direct"));
counter!("entrypoint_total", "path" => normalize_entry_path(path), "referrer" => referrer.to_string())
.increment(1);
}
@ -62,6 +62,22 @@ fn normalize_entry_path(path: &str) -> String {
}
}
fn normalize_referrer_label(referrer: &str) -> String {
let referrer = referrer.trim().trim_end_matches('.').to_ascii_lowercase();
if referrer.is_empty() || referrer == "direct" {
return "direct".to_string();
}
if referrer.len() > 120
|| !referrer
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '.' || ch == '-')
|| referrer.split('.').any(str::is_empty)
{
return "other".to_string();
}
referrer
}
fn parse_browser(ua: &str) -> String {
if ua.contains("Firefox") {
"Firefox".into()