Files
Jyotisha/frontend/src/lib/home-bootstrap-failure.ts
T

88 lines
3.1 KiB
TypeScript

/**
* Home bootstrap failures are not one "the API is slow" bucket.
*
* `api-error` is the in-bundle account/session path (HTTP status or the 8s
* cloud timeout). The other categories are what the inline dead-screen script
* can see before that bundle runs. Observation records only category, build
* id, and duration — never a payload, exception string, or birth data.
*/
export const HOME_BOOTSTRAP_FAILURE = {
hydrateTimeout: "hydrate-timeout",
chunk404: "chunk-404",
chunkParse: "chunk-parse",
staleCache: "stale-cache",
weakNetwork: "weak-network",
apiError: "api-error",
} as const;
export type HomeBootstrapFailureCategory =
(typeof HOME_BOOTSTRAP_FAILURE)[keyof typeof HOME_BOOTSTRAP_FAILURE];
export type HomeBootstrapFailureInput = {
/** `false` when the browser reports offline. `null` when the signal is absent. */
online: boolean | null;
scriptMessage?: string;
scriptSource?: string;
/** A failed script whose resource timing says it was served from cache. */
resourceFromCache?: boolean;
/** HTTP status from an account/session/model request the bundle actually made. */
apiStatus?: number | null;
/** Bundle-side cloud timeout (page.tsx 8000ms), not the dead-screen timer. */
apiTimedOut?: boolean;
};
const PARSE_FAILURE = /SyntaxError|Unexpected token|Invalid regular expression/i;
const CHUNK_LOAD_FAILURE = /ChunkLoadError|Loading chunk|Failed to fetch dynamically imported module|Failed to load/i;
export function sanitizeBootstrapBuildId(value: string | null | undefined): string {
if (typeof value !== "string") return "unknown";
const trimmed = value.trim();
return /^[0-9a-fA-F]{7,64}$/.test(trimmed) ? trimmed : "unknown";
}
export function classifyHomeBootstrapFailure(
input: HomeBootstrapFailureInput,
): HomeBootstrapFailureCategory {
if (typeof input.apiStatus === "number" && input.apiStatus >= 400) {
return HOME_BOOTSTRAP_FAILURE.apiError;
}
const message = input.scriptMessage ?? "";
const source = input.scriptSource ?? "";
const parseFailed = PARSE_FAILURE.test(message);
const chunkFailed = CHUNK_LOAD_FAILURE.test(`${message} ${source}`) || /\/_next\/static\//.test(source);
if (parseFailed) {
return input.resourceFromCache
? HOME_BOOTSTRAP_FAILURE.staleCache
: HOME_BOOTSTRAP_FAILURE.chunkParse;
}
if (chunkFailed) {
if (input.resourceFromCache) return HOME_BOOTSTRAP_FAILURE.staleCache;
if (input.online === false) return HOME_BOOTSTRAP_FAILURE.weakNetwork;
return HOME_BOOTSTRAP_FAILURE.chunk404;
}
if (input.online === false) return HOME_BOOTSTRAP_FAILURE.weakNetwork;
if (input.apiTimedOut) return HOME_BOOTSTRAP_FAILURE.apiError;
return HOME_BOOTSTRAP_FAILURE.hydrateTimeout;
}
export type BootstrapObservation = {
category: HomeBootstrapFailureCategory;
buildId: string;
durationMs: number;
};
export function buildBootstrapObservation(
category: HomeBootstrapFailureCategory,
buildId: string,
durationMs: number,
): BootstrapObservation {
const safeDuration = Number.isFinite(durationMs) ? Math.max(0, Math.round(durationMs)) : 0;
return {
category,
buildId: sanitizeBootstrapBuildId(buildId),
durationMs: safeDuration,
};
}