返回 DeepSeek-Reasonix
apply-firebase-crash.mjs
根目录 / workers / crash-report / scripts / apply-firebase-crash.mjs
1 #!/usr/bin/env node
2
3 import { spawnSync } from "node:child_process";
4 import { fileURLToPath } from "node:url";
5 import path from "node:path";
6 import { parseWranglerRows } from "./apply-diagnostics-v2.mjs";
7
8 export const firebaseCrashV1SchemaEntries = Object.freeze([
9 "table:firebase_crash_outbox",
10 "table:firebase_crash_receipts",
11 "index:firebase_crash_outbox_retry",
12 "index:firebase_crash_receipts_projected",
13 "table:firebase_crash_group_leases",
14 ]);
15
16 export const firebaseCrashV2SchemaEntries = Object.freeze([
17 "table:firebase_crash_group_state",
18 "index:firebase_crash_group_state_lifecycle",
19 "index:firebase_crash_group_state_lease",
20 ]);
21
22 export const firebaseCrashSchemaEntries = Object.freeze([
23 ...firebaseCrashV1SchemaEntries,
24 ...firebaseCrashV2SchemaEntries,
25 ]);
26
27 export const firebaseCrashSchemaQuery = `
28 SELECT type AS kind, name
29 FROM sqlite_master
30 WHERE type IN ('table', 'index') AND name IN (
31 'firebase_crash_outbox', 'firebase_crash_receipts', 'firebase_crash_group_leases',
32 'firebase_crash_outbox_retry', 'firebase_crash_receipts_projected',
33 'firebase_crash_group_state', 'firebase_crash_group_state_lifecycle',
34 'firebase_crash_group_state_lease'
35 )
36 ORDER BY kind, name;
37 `.trim();
38
39 export const firebaseCrashCapacityQuery = `
40 SELECT COALESCE(SUM(CASE
41 WHEN status IN ('resolved', 'ignored') AND datetime(last_seen) <= datetime('now', '-60 days') THEN 0
42 WHEN status IN ('resolved', 'ignored') AND datetime(last_seen) <= datetime('now', '-30 days') THEN 131072
43 ELSE 655360
44 END), 0) AS reserved_bytes
45 FROM groups;
46 `.trim();
47
48 function classifyEntries(rows, entries) {
49 const present = new Set(rows.map((row) => `${String(row.kind)}:${String(row.name)}`));
50 const missing = entries.filter((entry) => !present.has(entry));
51 if (missing.length === 0) return { state: "complete", missing };
52 if (missing.length === entries.length) return { state: "absent", missing };
53 return { state: "partial", missing };
54 }
55
56 export function classifyFirebaseCrashSchema(rows) {
57 const v1 = classifyEntries(rows, firebaseCrashV1SchemaEntries);
58 const v2 = classifyEntries(rows, firebaseCrashV2SchemaEntries);
59 const missing = [...v1.missing, ...v2.missing];
60 const state = v1.state === "complete" && v2.state === "complete"
61 ? "complete"
62 : v1.state === "absent" && v2.state === "absent" ? "absent" : "partial";
63 return { state, missing, v1, v2 };
64 }
65
66 function runWrangler(projectDir, args, captureOutput = false) {
67 const executable = process.platform === "win32" ? "wrangler.cmd" : "wrangler";
68 const wrangler = path.join(projectDir, "node_modules", ".bin", executable);
69 const result = spawnSync(wrangler, args, {
70 cwd: projectDir,
71 encoding: "utf8",
72 env: process.env,
73 stdio: captureOutput ? ["ignore", "pipe", "inherit"] : "inherit",
74 });
75 if (result.error) throw result.error;
76 if (result.status !== 0) throw new Error(`wrangler exited with status ${result.status}`);
77 return result.stdout ?? "";
78 }
79
80 function inspect(projectDir, database) {
81 const output = runWrangler(projectDir, [
82 "d1", "execute", database, "--remote", "--json", "--command", firebaseCrashSchemaQuery,
83 ], true);
84 return classifyFirebaseCrashSchema(parseWranglerRows(output));
85 }
86
87 function assertSparkCapacity(projectDir, database) {
88 const output = runWrangler(projectDir, [
89 "d1", "execute", database, "--remote", "--json", "--command", firebaseCrashCapacityQuery,
90 ], true);
91 const rows = parseWranglerRows(output);
92 const reserved = Number(rows[0]?.reserved_bytes ?? 0);
93 if (!Number.isFinite(reserved) || reserved > 700 * 1024 * 1024) {
94 throw new Error(`Firebase crash reservation preflight exceeds 700 MiB (${reserved} bytes)`);
95 }
96 }
97
98 function main() {
99 const projectDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
100 const database = process.env.DIAGNOSTICS_D1_DATABASE || "reasonix-crash";
101 const before = inspect(projectDir, database);
102 if (before.state === "complete") {
103 console.log("Firebase crash D1 schema is already complete; migration skipped.");
104 return;
105 }
106 if (before.v1.state === "partial" || before.v2.state === "partial" ||
107 (before.v1.state === "absent" && before.v2.state === "complete")) {
108 throw new Error(`Firebase crash D1 schema is partial; missing: ${before.missing.join(", ")}`);
109 }
110 console.log("Recording the current D1 Time Travel bookmark before migration.");
111 runWrangler(projectDir, ["d1", "time-travel", "info", database]);
112 if (before.v1.state === "absent") {
113 runWrangler(projectDir, [
114 "d1", "execute", database, "--remote", "--yes", "--file", "migrate-firebase-crash.sql",
115 ]);
116 }
117 if (before.v2.state === "absent") {
118 assertSparkCapacity(projectDir, database);
119 runWrangler(projectDir, [
120 "d1", "execute", database, "--remote", "--yes", "--file", "migrate-firebase-crash-capacity.sql",
121 ]);
122 }
123 const after = inspect(projectDir, database);
124 if (after.state !== "complete") {
125 throw new Error(`Firebase crash D1 migration failed; missing: ${after.missing.join(", ")}`);
126 }
127 console.log("Firebase crash D1 migration and verification completed.");
128 }
129
130 const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
131 if (invokedPath === fileURLToPath(import.meta.url)) {
132 try {
133 main();
134 } catch (error) {
135 console.error(error instanceof Error ? error.message : String(error));
136 process.exitCode = 1;
137 }
138 }
139
139 lines Plain Text