lgtm
This commit is contained in:
parent
a08b5d2ae0
commit
b98f0e3904
38 changed files with 3732 additions and 483 deletions
1
server-rs/Cargo.lock
generated
1
server-rs/Cargo.lock
generated
|
|
@ -3625,6 +3625,7 @@ dependencies = [
|
|||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue