返回 DeepSeek-Reasonix
report_classification.ts
根目录 / workers / crash-report / src / report_classification.ts
1 import { DEVELOPMENT_FINGERPRINT_PREFIX } from "./diagnostics_v2";
2 import type { ReportPayload } from "./report_schema";
3
4 const GROUP_PATH_RE = /^\/stats\/group\/((?:dev:)?[0-9a-f]{64})$/;
5 const RESIZE_OBSERVER_NOTICE_RE = /^ResizeObserver loop (?:limit exceeded|completed with undelivered notifications\.?)$/;
6
7 export type SeverityInput = {
8 kind: string;
9 version?: string;
10 source: string;
11 label: string;
12 errorType: string;
13 errorMessage: string;
14 topFrame: string;
15 channel?: string;
16 recovery?: string;
17 };
18
19 type ParsedVersion = {
20 version: string;
21 major: number;
22 minor: number;
23 patch: number;
24 };
25
26 export type RegressionDecision = "none" | "historical" | "suspected" | "confirmed";
27
28 // One-line human summary for the dashboard list. Frontend reports are formatted
29 // "[label]\n\n<detail>", so a bare label alone is folded together with its detail.
30 export function crashTitle(message: string): string {
31 const lines = message
32 .split("\n")
33 .map((line) => line.trim())
34 .filter(Boolean);
35 let head = lines[0] ?? "";
36 if (/^\[[^\]]+\]$/.test(head) && lines[1]) head = `${head} ${lines[1]}`;
37 return head.slice(0, 200);
38 }
39
40 export function isDevelopmentReport(input: SeverityInput): boolean {
41 const channel = input.channel?.trim().toLowerCase();
42 return channel === "dev" || channel === "test" || input.version?.trim().toLowerCase().startsWith("dev") === true;
43 }
44
45 export function reportSubjectIdentity(report: Pick<ReportPayload, "version" | "channel" | "diagnostics">): {
46 version: string;
47 channel: string;
48 } {
49 return {
50 version: report.diagnostics?.subjectVersion || report.version,
51 channel: report.diagnostics?.subjectChannel || report.channel || "",
52 };
53 }
54
55 export function namespaceReportFingerprint(hash: string, development: boolean): string {
56 return development ? `${DEVELOPMENT_FINGERPRINT_PREFIX}${hash}` : hash;
57 }
58
59 export function groupFingerprintFromPath(path: string): string | null {
60 return path.match(GROUP_PATH_RE)?.[1] ?? null;
61 }
62
63 export function isKnownNonCrashDiagnostic(input: SeverityInput): boolean {
64 const message = input.errorMessage.trim();
65 return (
66 RESIZE_OBSERVER_NOTICE_RE.test(message) ||
67 /Minified React error #520\b/.test(message) ||
68 message.includes("additional File object is not a file on the disk")
69 );
70 }
71
72 export function isOpaqueScriptErrorReport(input: SeverityInput): boolean {
73 return (
74 input.kind === "crash" &&
75 input.source === "frontend.global" &&
76 input.label === "window.error" &&
77 input.errorType === "string" &&
78 input.errorMessage.trim() === "Script error." &&
79 input.topFrame.trim() === ""
80 );
81 }
82
83 function severityForKind(kind: string): string {
84 if (kind === "crash") return "high";
85 if (kind === "performance" || kind === "bot" || kind === "exception") return "medium";
86 return "low";
87 }
88
89 export function severityForReport(input: SeverityInput): string {
90 if (isDevelopmentReport(input) || isOpaqueScriptErrorReport(input) || isKnownNonCrashDiagnostic(input)) return "low";
91 if ((input.source === "web.runtime.native" || input.source === "webview2.process.native") && input.recovery === "reload_succeeded") return "low";
92 if ((input.source === "web.runtime.native" || input.source === "webview2.process.native") && input.kind === "exception") return "high";
93 return severityForKind(input.kind);
94 }
95
96 export function severityRank(severity: string): number {
97 return ({ low: 1, medium: 2, high: 3, critical: 4 })[severity] ?? 0;
98 }
99
100 export function maxSeverity(current: string, incoming: string): string {
101 return severityRank(incoming) > severityRank(current) ? incoming : current;
102 }
103
104 function parseReleaseVersion(version: string): ParsedVersion | null {
105 // The latest lane is restricted to shipped stable builds. Development,
106 // prerelease, and build-metadata values remain visible only in facets.
107 const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)$/);
108 if (!match) return null;
109 return {
110 version,
111 major: Number(match[1]),
112 minor: Number(match[2]),
113 patch: Number(match[3]),
114 };
115 }
116
117 export function compareReleaseVersions(subject: string, fixedIn: string): number | null {
118 const subjectVersion = parseReleaseVersion(subject);
119 const fixedVersion = parseReleaseVersion(fixedIn);
120 if (!subjectVersion || !fixedVersion) return null;
121 return subjectVersion.major - fixedVersion.major || subjectVersion.minor - fixedVersion.minor || subjectVersion.patch - fixedVersion.patch;
122 }
123
124 export function regressionDecisionForReport(input: {
125 status?: string;
126 fixedIn?: string;
127 resolutionPlatform?: string;
128 resolutionRuntime?: string;
129 subjectVersion: string;
130 os: string;
131 runtime: string;
132 }): RegressionDecision {
133 if (input.status !== "resolved") return "none";
134 if (input.resolutionPlatform && input.resolutionPlatform !== input.os) return "none";
135 if (input.resolutionRuntime && input.resolutionRuntime !== input.runtime) return "none";
136
137 const comparison = compareReleaseVersions(input.subjectVersion, input.fixedIn ?? "");
138 if (comparison === null) return "suspected";
139 return comparison < 0 ? "historical" : "confirmed";
140 }
141
142 export function newestReleaseVersion(versions: string[]): string {
143 const parsed = versions
144 .filter((version) => version && version.toLowerCase() !== "dev")
145 .map(parseReleaseVersion)
146 .filter((version): version is ParsedVersion => version !== null);
147 parsed.sort(
148 (left, right) =>
149 right.major - left.major ||
150 right.minor - left.minor ||
151 right.patch - left.patch ||
152 right.version.localeCompare(left.version),
153 );
154 return parsed[0]?.version ?? "";
155 }
156
156 lines TYPESCRIPT