From 4796eb3072624920dbc6bac78b6596b90829fd0b Mon Sep 17 00:00:00 2001 From: Elie Habib Date: Thu, 26 Feb 2026 21:22:06 +0400 Subject: [PATCH 1/2] fix(sentry): tighten noise filters for deck.gl/maplibre and WebView errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Widen beforeSend regex to catch `null is not an object (evaluating 'u.id')` pattern from deck.gl internals during variant switch (WORLDMONITOR-4A, 270 events) - Remove `in_app` requirement from TypeError suppression — Sentry SDK marks deck.gl/maplibre frames inconsistently, causing the filter to miss - Fix Firefox lexical declaration wording: `can't access` vs Chrome's `Cannot access` - Add noise filters: isReCreate (Android WebView injection), HTMLImageElement style access, WebGL context loss write access --- src/main.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main.ts b/src/main.ts index d6e851fa0..765bfb76b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -97,7 +97,7 @@ Sentry.init({ /this\.St\.unref/, /Invalid or unexpected token/, /evaluating 'elemFound\.value'/, - /Cannot access '\w+' before initialization/, + /[Cc]an(?:'t|not) access (?:'\w+'|lexical declaration '\w+') before initialization/, /^Uint8Array$/, /createObjectStore/, /The database connection is closing/, @@ -108,19 +108,22 @@ Sentry.init({ /a2z\.onStatusUpdate/, /Attempting to run\(\), but is already running/, /this\.player\.destroy is not a function/, + /isReCreate is not defined/, + /reading 'style'.*HTMLImageElement/, + /can't access property "write", \w+ is undefined/, ], beforeSend(event) { const msg = event.exception?.values?.[0]?.value ?? ''; if (msg.length <= 3 && /^[a-zA-Z_$]+$/.test(msg)) return null; const frames = event.exception?.values?.[0]?.stacktrace?.frames ?? []; // Suppress maplibre internal null-access crashes (light, placement) only when stack is in map chunk - if (/this\.style\._layers|reading '_layers'|this\.light is null|can't access property "(id|type|setFilter)", \w+ is (null|undefined)|Cannot read properties of null \(reading '(id|type|setFilter|_layers)'\)|null is not an object \(evaluating '(E\.|this\.style)|^\w{1,2} is null$/.test(msg)) { + if (/this\.style\._layers|reading '_layers'|this\.light is null|can't access property "(id|type|setFilter)", \w+ is (null|undefined)|Cannot read properties of null \(reading '(id|type|setFilter|_layers)'\)|null is not an object \(evaluating '\w{1,3}\.(id|style)|^\w{1,2} is null$/.test(msg)) { if (frames.some(f => /\/(map|maplibre|deck-stack)-[A-Za-z0-9-]+\.js/.test(f.filename ?? ''))) return null; } // Suppress any TypeError that happens entirely within maplibre or deck.gl internals if (/^TypeError:/.test(msg) && frames.length > 0) { - const appFrames = frames.filter(f => f.in_app && !/\/sentry-[A-Za-z0-9-]+\.js/.test(f.filename ?? '')); - if (appFrames.length > 0 && appFrames.every(f => /\/(map|maplibre|deck-stack)-[A-Za-z0-9-]+\.js/.test(f.filename ?? ''))) return null; + const nonSentryFrames = frames.filter(f => !/\/sentry-[A-Za-z0-9-]+\.js/.test(f.filename ?? '')); + if (nonSentryFrames.length > 0 && nonSentryFrames.every(f => /\/(map|maplibre|deck-stack)-[A-Za-z0-9-]+\.js/.test(f.filename ?? ''))) return null; } // Suppress errors originating entirely from blob: URLs (browser extensions) if (frames.length > 0 && frames.every(f => /^blob:/.test(f.filename ?? ''))) return null; From bfe81861a6553d566c5af0edfb540938c47bf5eb Mon Sep 17 00:00:00 2001 From: Elie Habib Date: Thu, 26 Feb 2026 21:22:49 +0400 Subject: [PATCH 2/2] fix: reduce upstream API pressure with cache TTL optimization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Military/posture: 5min → 15min (flight cache, theater posture, panel refresh, intelligence refresh) - Theater posture: fetch 2 targeted bbox regions instead of global states/all (~95% less data) - Wingbits batch: reduce from 20 to 10, sequential with 100ms delay instead of Promise.all burst - Preserve intelligenceCache.military across intelligence refresh cycles - OpenSky edge proxy: add CDN caching (s-maxage=120), align timeout to 20s - list-military-flights: Redis cache 2min → 10min - Market handlers: stablecoins/crypto/commodities/sectors 3min → 5min - Cable health: 3min → 10min - YouTube embed: s-maxage 60s → 15min --- api/opensky.js | 4 +- api/youtube/embed.js | 2 +- .../infrastructure/v1/get-cable-health.ts | 2 +- .../market/v1/get-sector-summary.ts | 2 +- .../market/v1/list-commodity-quotes.ts | 2 +- .../market/v1/list-crypto-quotes.ts | 2 +- .../market/v1/list-stablecoin-markets.ts | 2 +- .../military/v1/get-aircraft-details-batch.ts | 16 +++-- .../military/v1/get-theater-posture.ts | 58 +++++++++++++------ .../military/v1/list-military-flights.ts | 2 +- src/App.ts | 4 +- src/components/StrategicPosturePanel.ts | 2 +- src/services/cached-theater-posture.ts | 2 +- src/services/military-flights.ts | 2 +- 14 files changed, 63 insertions(+), 39 deletions(-) diff --git a/api/opensky.js b/api/opensky.js index 7d7b70377..a78dcc8fe 100644 --- a/api/opensky.js +++ b/api/opensky.js @@ -19,7 +19,7 @@ function getRelayHeaders(baseHeaders = {}) { return headers; } -async function fetchWithTimeout(url, options, timeoutMs = 15000) { +async function fetchWithTimeout(url, options, timeoutMs = 20000) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { @@ -67,7 +67,7 @@ export default async function handler(req) { const body = await response.text(); const headers = { 'Content-Type': response.headers.get('content-type') || 'application/json', - 'Cache-Control': response.headers.get('cache-control') || 'no-cache', + 'Cache-Control': 'public, s-maxage=120, stale-while-revalidate=60', ...corsHeaders, }; const xCache = response.headers.get('x-cache'); diff --git a/api/youtube/embed.js b/api/youtube/embed.js index 913eb828c..38bdbd439 100644 --- a/api/youtube/embed.js +++ b/api/youtube/embed.js @@ -158,7 +158,7 @@ export default async function handler(request) { status: 200, headers: { 'content-type': 'text/html; charset=utf-8', - 'cache-control': 'public, s-maxage=60, stale-while-revalidate=300', + 'cache-control': 'public, s-maxage=900, stale-while-revalidate=300', }, }); } diff --git a/server/worldmonitor/infrastructure/v1/get-cable-health.ts b/server/worldmonitor/infrastructure/v1/get-cable-health.ts index 6e4200b12..0b08bf2c7 100644 --- a/server/worldmonitor/infrastructure/v1/get-cable-health.ts +++ b/server/worldmonitor/infrastructure/v1/get-cable-health.ts @@ -16,7 +16,7 @@ import { CHROME_UA } from '../../../_shared/constants'; // ======================================================================== const CACHE_KEY = 'cable-health-v1'; -const CACHE_TTL = 180; // 3 minutes +const CACHE_TTL = 600; // 10 min — cable health not time-critical // In-memory fallback: serves stale data when both Redis and NGA are down let fallbackCache: GetCableHealthResponse | null = null; diff --git a/server/worldmonitor/market/v1/get-sector-summary.ts b/server/worldmonitor/market/v1/get-sector-summary.ts index 8833c23ab..669e3a33e 100644 --- a/server/worldmonitor/market/v1/get-sector-summary.ts +++ b/server/worldmonitor/market/v1/get-sector-summary.ts @@ -15,7 +15,7 @@ import { fetchFinnhubQuote, fetchYahooQuotesBatch } from './_shared'; import { cachedFetchJson } from '../../../_shared/redis'; const REDIS_CACHE_KEY = 'market:sectors:v1'; -const REDIS_CACHE_TTL = 180; // 3 min — Finnhub rate-limited +const REDIS_CACHE_TTL = 300; // 5 min — Finnhub rate-limited export async function getSectorSummary( _ctx: ServerContext, diff --git a/server/worldmonitor/market/v1/list-commodity-quotes.ts b/server/worldmonitor/market/v1/list-commodity-quotes.ts index 0d2f19c6a..4836dde82 100644 --- a/server/worldmonitor/market/v1/list-commodity-quotes.ts +++ b/server/worldmonitor/market/v1/list-commodity-quotes.ts @@ -13,7 +13,7 @@ import { fetchYahooQuotesBatch } from './_shared'; import { cachedFetchJson } from '../../../_shared/redis'; const REDIS_CACHE_KEY = 'market:commodities:v1'; -const REDIS_CACHE_TTL = 180; // 3 min — commodities move slower than indices +const REDIS_CACHE_TTL = 300; // 5 min — commodities move slower than indices function redisCacheKey(symbols: string[]): string { return `${REDIS_CACHE_KEY}:${[...symbols].sort().join(',')}`; diff --git a/server/worldmonitor/market/v1/list-crypto-quotes.ts b/server/worldmonitor/market/v1/list-crypto-quotes.ts index 8a72c12ea..04159e9ab 100644 --- a/server/worldmonitor/market/v1/list-crypto-quotes.ts +++ b/server/worldmonitor/market/v1/list-crypto-quotes.ts @@ -13,7 +13,7 @@ import { CRYPTO_META, fetchCoinGeckoMarkets } from './_shared'; import { cachedFetchJson } from '../../../_shared/redis'; const REDIS_CACHE_KEY = 'market:crypto:v1'; -const REDIS_CACHE_TTL = 180; // 3 min — CoinGecko rate-limited +const REDIS_CACHE_TTL = 300; // 5 min — CoinGecko rate-limited export async function listCryptoQuotes( _ctx: ServerContext, diff --git a/server/worldmonitor/market/v1/list-stablecoin-markets.ts b/server/worldmonitor/market/v1/list-stablecoin-markets.ts index 50c9b29e2..a4ce3a1d3 100644 --- a/server/worldmonitor/market/v1/list-stablecoin-markets.ts +++ b/server/worldmonitor/market/v1/list-stablecoin-markets.ts @@ -14,7 +14,7 @@ import { CHROME_UA } from '../../../_shared/constants'; import { cachedFetchJson } from '../../../_shared/redis'; const REDIS_CACHE_KEY = 'market:stablecoins:v1'; -const REDIS_CACHE_TTL = 180; // 3 min — CoinGecko rate-limited +const REDIS_CACHE_TTL = 300; // 5 min — CoinGecko rate-limited // ======================================================================== // Constants and cache diff --git a/server/worldmonitor/military/v1/get-aircraft-details-batch.ts b/server/worldmonitor/military/v1/get-aircraft-details-batch.ts index 0a2c54131..d180f1f10 100644 --- a/server/worldmonitor/military/v1/get-aircraft-details-batch.ts +++ b/server/worldmonitor/military/v1/get-aircraft-details-batch.ts @@ -27,7 +27,7 @@ export async function getAircraftDetailsBatch( .map((id) => id.trim().toLowerCase()) .filter((id) => id.length > 0); const uniqueSorted = Array.from(new Set(normalized)).sort(); - const limitedList = uniqueSorted.slice(0, 20); + const limitedList = uniqueSorted.slice(0, 10); // Redis shared cache — batch GET all keys in a single pipeline round-trip const SINGLE_KEY = 'military:aircraft:v1'; @@ -52,7 +52,10 @@ export async function getAircraftDetailsBatch( } } - const fetches = toFetch.map(async (icao24) => { + const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); + + for (let i = 0; i < toFetch.length; i++) { + const icao24 = toFetch[i]!; const cacheResult = await cachedFetchJson( `${SINGLE_KEY}:${icao24}`, SINGLE_TTL, @@ -74,13 +77,8 @@ export async function getAircraftDetailsBatch( return null; }, ); - if (cacheResult?.details) return { icao24, details: cacheResult.details }; - return null; - }); - - const fetchResults = await Promise.all(fetches); - for (const r of fetchResults) { - if (r) results[r.icao24] = r.details; + if (cacheResult?.details) results[icao24] = cacheResult.details; + if (i < toFetch.length - 1) await delay(100); } return { diff --git a/server/worldmonitor/military/v1/get-theater-posture.ts b/server/worldmonitor/military/v1/get-theater-posture.ts index ea0256a9e..5f229dd2f 100644 --- a/server/worldmonitor/military/v1/get-theater-posture.ts +++ b/server/worldmonitor/military/v1/get-theater-posture.ts @@ -21,7 +21,7 @@ import { CHROME_UA } from '../../../_shared/constants'; const CACHE_KEY = 'theater-posture:sebuf:v1'; const STALE_CACHE_KEY = 'theater-posture:sebuf:stale:v1'; const BACKUP_CACHE_KEY = 'theater-posture:sebuf:backup:v1'; -const CACHE_TTL = 300; +const CACHE_TTL = 900; // 15 minutes const STALE_TTL = 86400; const BACKUP_TTL = 604800; @@ -43,23 +43,17 @@ function getRelayRequestHeaders(): Record { return headers; } -async function fetchMilitaryFlightsFromOpenSky(): Promise { - const isSidecar = (process.env.LOCAL_API_MODE || '').includes('sidecar'); - const baseUrl = isSidecar - ? 'https://opensky-network.org/api/states/all' - : process.env.WS_RELAY_URL ? process.env.WS_RELAY_URL + '/opensky' : null; - - if (!baseUrl) return []; +// Two bounding boxes covering all 9 POSTURE_THEATERS instead of fetching every +// aircraft globally. Returns ~hundreds of relevant states instead of ~10,000+. +const THEATER_QUERY_REGIONS = [ + { name: 'WESTERN', lamin: 10, lamax: 66, lomin: 9, lomax: 66 }, // Baltic→Yemen, Baltic→Iran + { name: 'PACIFIC', lamin: 4, lamax: 44, lomin: 104, lomax: 133 }, // SCS→Korea +]; - const resp = await fetch(baseUrl, { - headers: getRelayRequestHeaders(), - signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS), - }); - if (!resp.ok) throw new Error(`OpenSky API error: ${resp.status}`); - - const data = (await resp.json()) as { states?: Array<[string, string, ...unknown[]]> }; +function parseOpenSkyStates( + data: { states?: Array<[string, string, ...unknown[]]> }, +): RawFlight[] { if (!data.states) return []; - const flights: RawFlight[] = []; for (const state of data.states) { const [icao24, callsign, , , , lon, lat, altitude, onGround, velocity, heading] = state as [ @@ -67,7 +61,6 @@ async function fetchMilitaryFlightsFromOpenSky(): Promise { ]; if (lat == null || lon == null || onGround) continue; if (!isMilitaryCallsign(callsign) && !isMilitaryHex(icao24)) continue; - flights.push({ id: icao24, callsign: callsign?.trim() || '', @@ -81,6 +74,37 @@ async function fetchMilitaryFlightsFromOpenSky(): Promise { return flights; } +async function fetchMilitaryFlightsFromOpenSky(): Promise { + const isSidecar = (process.env.LOCAL_API_MODE || '').includes('sidecar'); + const baseUrl = isSidecar + ? 'https://opensky-network.org/api/states/all' + : process.env.WS_RELAY_URL ? process.env.WS_RELAY_URL + '/opensky' : null; + + if (!baseUrl) return []; + + const seenIds = new Set(); + const allFlights: RawFlight[] = []; + + for (const region of THEATER_QUERY_REGIONS) { + const params = `lamin=${region.lamin}&lamax=${region.lamax}&lomin=${region.lomin}&lomax=${region.lomax}`; + const resp = await fetch(`${baseUrl}?${params}`, { + headers: getRelayRequestHeaders(), + signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS), + }); + if (!resp.ok) throw new Error(`OpenSky API error: ${resp.status} for ${region.name}`); + + const data = (await resp.json()) as { states?: Array<[string, string, ...unknown[]]> }; + for (const flight of parseOpenSkyStates(data)) { + if (!seenIds.has(flight.id)) { + seenIds.add(flight.id); + allFlights.push(flight); + } + } + } + + return allFlights; +} + async function fetchMilitaryFlightsFromWingbits(): Promise { const apiKey = process.env.WINGBITS_API_KEY; if (!apiKey) return null; diff --git a/server/worldmonitor/military/v1/list-military-flights.ts b/server/worldmonitor/military/v1/list-military-flights.ts index f36d52505..512f835dc 100644 --- a/server/worldmonitor/military/v1/list-military-flights.ts +++ b/server/worldmonitor/military/v1/list-military-flights.ts @@ -12,7 +12,7 @@ import { CHROME_UA } from '../../../_shared/constants'; import { cachedFetchJson } from '../../../_shared/redis'; const REDIS_CACHE_KEY = 'military:flights:v1'; -const REDIS_CACHE_TTL = 120; // 2 min — real-time ADS-B data +const REDIS_CACHE_TTL = 600; // 10 min — reduce upstream API pressure /** Snap a coordinate to a grid step so nearby bbox values share cache entries. */ const quantize = (v: number, step: number) => Math.round(v / step) * step; diff --git a/src/App.ts b/src/App.ts index bee9eac2f..e5541e1fc 100644 --- a/src/App.ts +++ b/src/App.ts @@ -508,9 +508,11 @@ export class App { // Refresh intelligence signals for CII (geopolitical variant only) if (SITE_VARIANT === 'full') { this.refreshScheduler.scheduleRefresh('intelligence', () => { + const { military } = this.state.intelligenceCache; this.state.intelligenceCache = {}; + if (military) this.state.intelligenceCache.military = military; return this.dataLoader.loadIntelligenceSignals(); - }, 5 * 60 * 1000); + }, 15 * 60 * 1000); } } } diff --git a/src/components/StrategicPosturePanel.ts b/src/components/StrategicPosturePanel.ts index 7a9069cbd..dd327ed93 100644 --- a/src/components/StrategicPosturePanel.ts +++ b/src/components/StrategicPosturePanel.ts @@ -42,7 +42,7 @@ export class StrategicPosturePanel extends Panel { this.refreshInterval = setInterval(() => { if (!this.isPanelVisible()) return; void this.fetchAndRender(); - }, 5 * 60 * 1000); + }, 15 * 60 * 1000); } private isPanelVisible(): boolean { diff --git a/src/services/cached-theater-posture.ts b/src/services/cached-theater-posture.ts index 552ae0a0f..e9de740c3 100644 --- a/src/services/cached-theater-posture.ts +++ b/src/services/cached-theater-posture.ts @@ -114,7 +114,7 @@ const LS_MAX_AGE_MS = 30 * 60 * 1000; // 30 min max staleness for localStorage let cachedPosture: CachedTheaterPosture | null = null; let fetchPromise: Promise | null = null; let lastFetchTime = 0; -const REFETCH_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes (matches server TTL) +const REFETCH_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes - reduce upstream API pressure function createAbortError(): DOMException { return new DOMException('The operation was aborted.', 'AbortError'); diff --git a/src/services/military-flights.ts b/src/services/military-flights.ts index 03ec04b3a..1fc2dfd41 100644 --- a/src/services/military-flights.ts +++ b/src/services/military-flights.ts @@ -25,7 +25,7 @@ const DIRECT_OPENSKY_BASE_URL = wsRelayUrl const isLocalhostRuntime = typeof window !== 'undefined' && ['localhost', '127.0.0.1'].includes(window.location.hostname); // Cache configuration -const CACHE_TTL = 5 * 60 * 1000; // 5 minutes - match refresh interval +const CACHE_TTL = 15 * 60 * 1000; // 15 minutes - reduce upstream API pressure let flightCache: { data: MilitaryFlight[]; timestamp: number } | null = null; // Track flight history for trails