Files
workinf_Blender_Wasm/web/protocol/diagnostic-report.ts
mes123456 0fe8d2bb56
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

161 lines
6.6 KiB
TypeScript

export const APP_DIAGNOSTIC_SCHEMA_VERSION = 1 as const;
export const MAX_APP_DIAGNOSTIC_ENTRIES = 200;
export const APP_DIAGNOSTIC_MESSAGES = {
VIEWPORT_INIT_FAILED: "Viewport: unavailable",
ACTION_CONFLICT: "Action: another project operation is running",
ACTION_LOCK_FAILED: "Action: retry required",
RECENT_PROJECT_LIST_FAILED: "Storage: recent projects unavailable",
RECENT_PROJECT_REPAIR_FAILED: "Storage: recent project repair failed",
RECENT_PROJECT_UPDATE_FAILED: "Storage: recent project update failed",
STORAGE_BUDGET_FAILED: "Storage: budget unavailable",
STORAGE_CLEANUP_FAILED: "Storage: cleanup failed",
OPERATION_LOG_FAILED: "Storage: operation log unavailable",
COMMAND_FAILED: "Engine: command failed",
IMAGE_IMPORT_FAILED: "Image import failed",
GREASE_PENCIL_EDIT_FAILED: "Grease Pencil edit failed",
CURVE_EDIT_FAILED: "Curve edit failed",
LOD_CACHE_READ_FAILED: "Storage: LOD cache unavailable; generated geometry retained",
LOD_GENERATION_FAILED: "Engine: LOD generation failed",
ENGINE_WORKER_TERMINATED: "Engine: Worker stopped; project remains available",
ENGINE_START_FAILED: "Engine: unavailable",
MANIFEST_REJECTED: "Manifest: rejected",
STORAGE_WORKER_TERMINATED: "Storage: Worker stopped; project remains available",
STORAGE_START_FAILED: "Storage: unavailable",
PBR_ASSET_INVALID: "PBR asset unavailable",
BLEND_OPEN_FAILED: "Engine: .blend open failed",
POST_COMMIT_MAINTENANCE_FAILED: "Storage: post-commit maintenance failed",
PROJECT_RECOVERY_FAILED: "Recovery: project could not be restored",
WORKER_RECOVERY_FAILED: "Recovery: Worker restart failed",
BLEND_SAVE_FAILED: "Engine: .blend save failed",
BLEND_DOWNLOAD_FAILED: "Engine: .blend download failed",
GLB_PROJECT_UNAVAILABLE: "GLB: no open project",
GLB_EXPORT_BLOCKED: "GLB: export blocked",
GLB_EXPORT_FAILED: "GLB: export failed",
AUTOSAVE_FAILED: "Engine: autosave failed",
} as const;
export type AppDiagnosticCode = keyof typeof APP_DIAGNOSTIC_MESSAGES;
export type AppDiagnosticArea = "ACTION" | "ENGINE" | "STORAGE" | "VIEWPORT" | "EXPORT" | "RUNTIME";
export type AppDiagnosticContextValue = string | number | boolean | null;
export interface AppDiagnosticEntry {
schemaVersion: typeof APP_DIAGNOSTIC_SCHEMA_VERSION;
sequence: number;
occurredAt: string;
area: AppDiagnosticArea;
code: AppDiagnosticCode;
summary: string;
detail: string;
sourceCode?: string;
stack?: string;
cause?: string;
context?: Record<string, AppDiagnosticContextValue>;
}
export interface AppDiagnosticReport {
schemaVersion: typeof APP_DIAGNOSTIC_SCHEMA_VERSION;
product: "Web Blender Modeler V1";
generatedAt: string;
runtime: {
url: string;
userAgent: string;
language: string;
crossOriginIsolated: boolean;
};
project: {
projectId: string;
revision: number;
};
entries: AppDiagnosticEntry[];
}
function objectString(value: unknown): string | undefined {
if (typeof value !== "object" || value === null) return undefined;
try {
const seen = new WeakSet<object>();
return JSON.stringify(value, (_key, candidate: unknown) => {
if (candidate instanceof Error) {
return { name: candidate.name, message: candidate.message, stack: candidate.stack, cause: candidate.cause };
}
if (typeof candidate === "object" && candidate !== null) {
if (seen.has(candidate)) return "[Circular]";
seen.add(candidate);
}
return candidate;
});
}
catch {
return "[Unserializable diagnostic detail]";
}
}
function propertyString(value: unknown, property: string): string | undefined {
if (typeof value !== "object" || value === null || !(property in value)) return undefined;
const candidate = (value as Record<string, unknown>)[property];
return typeof candidate === "string" && candidate ? candidate : undefined;
}
function diagnosticDetail(error: unknown): Pick<AppDiagnosticEntry, "detail" | "sourceCode" | "stack" | "cause"> {
if (error instanceof Error) {
return {
detail: error.message || error.name,
sourceCode: propertyString(error, "code"),
stack: error.stack,
cause: error.cause instanceof Error ? error.cause.message : typeof error.cause === "string" ? error.cause : objectString(error.cause),
};
}
if (typeof error === "string") return { detail: error };
const message = propertyString(error, "message");
const extraDetail = propertyString(error, "detail");
return {
detail: [message, extraDetail].filter(Boolean).join("; ") || objectString(error) || String(error),
sourceCode: propertyString(error, "code"),
stack: propertyString(error, "stack"),
cause: propertyString(error, "cause"),
};
}
export function createAppDiagnosticEntry(input: {
sequence: number;
occurredAt: string;
area: AppDiagnosticArea;
code: AppDiagnosticCode;
error: unknown;
context?: Record<string, AppDiagnosticContextValue>;
}): AppDiagnosticEntry {
if (!Number.isSafeInteger(input.sequence) || input.sequence <= 0) throw new Error("APP_DIAGNOSTIC_SEQUENCE_INVALID");
if (!Number.isFinite(Date.parse(input.occurredAt)) || new Date(input.occurredAt).toISOString() !== input.occurredAt) throw new Error("APP_DIAGNOSTIC_TIMESTAMP_INVALID");
const detail = diagnosticDetail(input.error);
return {
schemaVersion: APP_DIAGNOSTIC_SCHEMA_VERSION,
sequence: input.sequence,
occurredAt: input.occurredAt,
area: input.area,
code: input.code,
summary: APP_DIAGNOSTIC_MESSAGES[input.code],
detail: detail.detail,
...(detail.sourceCode ? { sourceCode: detail.sourceCode } : {}),
...(detail.stack ? { stack: detail.stack } : {}),
...(detail.cause ? { cause: detail.cause } : {}),
...(input.context ? { context: { ...input.context } } : {}),
};
}
export function appendAppDiagnostic(entries: readonly AppDiagnosticEntry[], entry: AppDiagnosticEntry, limit = MAX_APP_DIAGNOSTIC_ENTRIES): AppDiagnosticEntry[] {
if (!Number.isSafeInteger(limit) || limit <= 0) throw new Error("APP_DIAGNOSTIC_LIMIT_INVALID");
return [...entries, entry].slice(-limit);
}
export function createAppDiagnosticReport(input: Omit<AppDiagnosticReport, "schemaVersion" | "product" | "entries"> & { entries: readonly AppDiagnosticEntry[] }): AppDiagnosticReport {
if (!Number.isFinite(Date.parse(input.generatedAt)) || new Date(input.generatedAt).toISOString() !== input.generatedAt) throw new Error("APP_DIAGNOSTIC_REPORT_TIMESTAMP_INVALID");
return {
schemaVersion: APP_DIAGNOSTIC_SCHEMA_VERSION,
product: "Web Blender Modeler V1",
generatedAt: input.generatedAt,
runtime: { ...input.runtime },
project: { ...input.project },
entries: [...input.entries].sort((left, right) => left.sequence - right.sequence),
};
}