| 1 | import { esc } from "./shell"; |
| 2 | |
| 3 | export function i18n(en: string, zh: string): string { |
| 4 | return `<span data-i18n="en">${esc(en)}</span><span data-i18n="zh">${esc(zh)}</span>`; |
| 5 | } |
| 6 | |
| 7 | export function i18nHTML(en: string, zh: string): string { |
| 8 | return `<span data-i18n="en">${en}</span><span data-i18n="zh">${zh}</span>`; |
| 9 | } |
| 10 | |
| 11 | export type CrashRow = { |
| 12 | fingerprint: string; |
| 13 | kind: string; |
| 14 | count: number; |
| 15 | first_version: string; |
| 16 | last_version: string; |
| 17 | seen: string; |
| 18 | status: string; |
| 19 | title: string; |
| 20 | source: string; |
| 21 | label: string; |
| 22 | error_type: string; |
| 23 | top_frame: string; |
| 24 | severity: string; |
| 25 | last_os: string; |
| 26 | last_arch: string; |
| 27 | last_channel: string; |
| 28 | regressed_at: string; |
| 29 | last_category?: string; |
| 30 | development?: boolean; |
| 31 | affected_installs?: number; |
| 32 | window_events?: number; |
| 33 | identified_events?: number; |
| 34 | identity_coverage?: number; |
| 35 | dimension_coverage?: number; |
| 36 | impact_rate?: number | null; |
| 37 | }; |
| 38 | |
| 39 | export function clip(s: string, n: number): string { |
| 40 | return s.length > n ? `${s.slice(0, n - 1)}…` : s; |
| 41 | } |
| 42 | |
| 43 | export function statusPill(status: string): string { |
| 44 | if (status === "resolved") return `<span class="pill status-resolved">${i18n("Resolved", "已解决")}</span>`; |
| 45 | if (status === "ignored") return `<span class="pill status-ignored">${i18n("Ignored", "已忽略")}</span>`; |
| 46 | return `<span class="pill status-open open">${i18n("Open", "未处理")}</span>`; |
| 47 | } |
| 48 | |
| 49 | function crashMetric(en: string, zh: string, value: string | number, className = ""): string { |
| 50 | return `<span class="crash-metric${className ? ` ${className}` : ""}"><b>${esc(value)}</b><small>${i18n(en, zh)}</small></span>`; |
| 51 | } |
| 52 | |
| 53 | export function reportGroups(rows: CrashRow[], compact = false, windowDays: 7 | 30 = 30): string { |
| 54 | if (!rows.length) return `<div class="empty">${i18n("No diagnostic reports yet — that's the good kind of empty", "还没有诊断报告,这是好消息")}</div>`; |
| 55 | return `<div class="crash-list${compact ? " compact" : ""}"><div class="crash-head"><span>${i18n("summary", "摘要")}</span><span>${i18n("scope", "范围")}</span><span>${i18n("health", "状态")}</span><span title="${i18n("Window events, impact rate, identity coverage, and lifetime count", "窗口事件、影响率、身份覆盖率和累计事件")}">${i18n("triage metrics", "分诊指标")}</span></div>${rows |
| 56 | .map((c) => { |
| 57 | const platform = [c.last_os, c.last_arch].filter(Boolean).join("/"); |
| 58 | const versions = `${c.first_version || "?"} → ${c.last_version || "?"}`; |
| 59 | const title = c.title || c.error_type || c.top_frame || c.fingerprint; |
| 60 | const identityCoverage = Number(c.identity_coverage ?? 0) >= 0.9 && Number(c.dimension_coverage ?? 1) >= 0.9; |
| 61 | const impactRate = c.impact_rate !== null && c.impact_rate !== undefined ? `${(Number(c.impact_rate) * 100).toFixed(1)}%` : "—"; |
| 62 | return `<a class="crash-item" href="/stats/group/${esc(c.fingerprint)}" title="${esc(title)}"> |
| 63 | <span class="crash-summary" aria-label="${esc(title)}"><span>${c.title ? esc(clip(c.title, compact ? 150 : 180)) : `<span class="muted">${i18n("No summary captured", "暂无摘要")}</span>`}</span><small>${esc(c.fingerprint.slice(0, 8))} · ${esc(c.seen)}</small>${ |
| 64 | c.regressed_at ? `<em>${i18nHTML(`regressed ${esc(c.regressed_at.slice(0, 10))}`, `回归 ${esc(c.regressed_at.slice(0, 10))}`)}</em>` : "" |
| 65 | }</span> |
| 66 | <span class="crash-scope"><span class="crash-meta"><b>${i18n("Source", "来源")}</b><span>${esc(c.source || "legacy")}</span></span><span class="crash-meta"><b>${i18n("Versions", "版本")}</b><span>${esc(versions)}</span></span><span class="crash-meta"><b>${i18n("Platform", "平台")}</b><span>${platform ? esc(platform) : "unknown platform"}</span></span>${c.last_channel && c.last_channel !== "stable" ? `<span class="crash-meta"><b>${i18n("Channel", "渠道")}</b><span>${esc(c.last_channel)}</span></span>` : ""}</span> |
| 67 | <span class="crash-health"><span class="pill">${esc(c.severity || "medium")}</span><span class="pill ${c.kind === "crash" ? "crash" : ""}">${esc(c.kind)}</span>${statusPill(c.status)}</span> |
| 68 | <span class="crash-count"><span class="crash-metrics">${crashMetric(`Affected installs (${windowDays}d)`, `受影响安装(${windowDays}天)`, Number(c.affected_installs ?? 0), "primary")}${crashMetric("Impact rate", "影响率", impactRate)}${crashMetric("Window events", "窗口事件", Number(c.window_events ?? 0))}${crashMetric("Identity coverage", "身份覆盖率", identityCoverage ? `${Math.round(Number(c.identity_coverage) * 100)}%` : "sample incomplete / 样本不完整")}${crashMetric("Lifetime events", "累计事件", Number(c.count), "full")}</span></span> |
| 69 | </a>`; |
| 70 | }) |
| 71 | .join("")}</div>`; |
| 72 | } |
| 73 |