88 lines
3.1 KiB
TypeScript
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,
|
|
};
|
|
}
|