fix: surface silent scraper failures and stop falsely claiming weather delay
The homepage was flagging every park as weather delay because calendar.ts
collapsed "fetchLiveRides returned null" into the same openRides=0 bucket as
"all rides actually closed." Meanwhile every scraper (queuetimes, sixflags
operating-hours, sixflags wait-times) was swallowing non-OK responses and
exceptions silently, so logs gave no signal which upstream was failing or how.
Add a small scraperWarn helper that emits in the same shape as backend/log.ts
(without importing it — lib/scrapers is shared with the Next frontend). Use it
in all three scrapers to record HTTP status and error name+message before each
return null. Add parksSkipped to the tier-5 summary log so we can tell when the
openParks filter is rejecting everyone vs the fetcher silently failing.
Convert calendar.ts ridesCache to a discriminated union { kind: "ok" | "unknown" }.
Weather delay only fires on { kind: "ok", openRides: 0 }; unknown entries get
a 30s TTL so we recover quickly when upstream comes back and don't thunder-herd
in the meantime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,8 @@
|
||||
* Rate limiting: on 429/503, exponential backoff (30s → 60s → 120s), MAX_RETRIES attempts.
|
||||
*/
|
||||
|
||||
import { scraperWarn } from "./log";
|
||||
|
||||
const API_BASE = "https://d18car1k0ff81h.cloudfront.net/operating-hours/park";
|
||||
const MAX_RETRIES = 3;
|
||||
const BASE_BACKOFF_MS = 30_000;
|
||||
@@ -191,9 +193,18 @@ export async function fetchToday(apiId: number, revalidate?: number): Promise<Da
|
||||
try {
|
||||
const url = `${API_BASE}/${apiId}`;
|
||||
const raw = await fetchApi(url, 0, 0, revalidate);
|
||||
if (!raw.dates.length) return null;
|
||||
if (!raw.dates.length) {
|
||||
scraperWarn("sixflags", "fetchToday empty dates array", { apiId });
|
||||
return null;
|
||||
}
|
||||
return parseApiDay(raw.dates[0]);
|
||||
} catch {
|
||||
} catch (err) {
|
||||
const e = err as Error;
|
||||
scraperWarn("sixflags", "fetchToday threw", {
|
||||
apiId,
|
||||
name: e.name,
|
||||
err: e.message,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -224,11 +235,22 @@ export async function scrapeRidesForDay(
|
||||
let raw: ApiResponse;
|
||||
try {
|
||||
raw = await scrapeMonthRaw(apiId, year, month, revalidate);
|
||||
} catch {
|
||||
} catch (err) {
|
||||
const e = err as Error;
|
||||
scraperWarn("sixflags", "scrapeRidesForDay scrapeMonthRaw threw", {
|
||||
apiId,
|
||||
year,
|
||||
month,
|
||||
name: e.name,
|
||||
err: e.message,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!raw.dates.length) return null;
|
||||
if (!raw.dates.length) {
|
||||
scraperWarn("sixflags", "scrapeRidesForDay empty dates array", { apiId, year, month });
|
||||
return null;
|
||||
}
|
||||
|
||||
// The API uses "MM/DD/YYYY" internally.
|
||||
const [, mm, dd] = dateIso.split("-");
|
||||
@@ -260,8 +282,15 @@ export async function scrapeRidesForDay(
|
||||
const nextRaw = await scrapeMonthRaw(apiId, nextYear, nextMonth, revalidate);
|
||||
const nextSorted = [...nextRaw.dates].sort((a, b) => a.date.localeCompare(b.date));
|
||||
dayData = nextSorted.find((d) => !d.isParkClosed) ?? nextSorted[0];
|
||||
} catch {
|
||||
// If the next month fetch fails, we simply have no fallback data.
|
||||
} catch (err) {
|
||||
const e = err as Error;
|
||||
scraperWarn("sixflags", "scrapeRidesForDay next-month fallback threw", {
|
||||
apiId,
|
||||
year: nextYear,
|
||||
month: nextMonth,
|
||||
name: e.name,
|
||||
err: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user