From a634558bc2d19cff4baf09f5ba2f65839b64736c Mon Sep 17 00:00:00 2001 From: Andrew Mikofalvy <5668128+amikofalvy@users.noreply.github.com> Date: Mon, 20 Jul 2026 00:09:26 -0700 Subject: [PATCH] fix(bug-report): include the crash minidump in crash-triggered reports (#2726) A crash-detected report invitation (boot-time dirty-shutdown/minidump scan or a live render/child process death) is precisely a request to report a crash, yet the crash-dump opt-in defaulted OFF, so crash reports usually arrived without the minidump that triggered them (PRD-7403, split from PRD-7394). The attach mechanism was fully wired; only the default did not serve the crash-report case. Make the crash-dump opt-in appear only when main confirms a minidump is on disk for the event, and default it ON when it does: - Thread an authoritative minidumpAvailable signal from main onto OkBugReportCrashDetectedEvent (only main can stat the crash-dumps dir). Boot events derive it from the freshness scan; process-gone events check the dir at arm time (best-effort: Crashpad may still be flushing). - The report dialog renders the crash-dump row only when minidumpAvailable is true, defaults it checked, and sends includeCrashDump only then. A dump-less invite (e.g. a dirty shutdown with no native dump) shows no dead checkbox. Plain compose is unchanged and still carries no dump. Consent is preserved without a silent send: the row stays a visible, uncheckable checkbox, its note still states the memory is unredactable and can contain document content, and nothing leaves the machine until the user reviews the exact bundle (whose review card flags "crash dump not redacted"). The no-silent-send invariant (N7) is kept. NOTE FOR REVIEW: this flips the deliberately locked default-OFF from spec 2026-06-27-ok-report-bug-experience (R13f / OQ12 / D18), which lists "defaulting minidump inclusion to ON" as an ASK_FIRST guardrail. Approved by Andrew for the detected-crash + dump-available case specifically. GitOrigin-RevId: 9027463f803660cee61c5bbcfcf4b4c2cf35f9da --- .changeset/crash-invite-includes-minidump.md | 9 +++++ .../ReportBugCrashInviteTrigger.dom.test.tsx | 1 + .../components/ReportBugDialog.dom.test.tsx | 39 ++++++++++++++---- .../src/components/ReportBugDialogBody.tsx | 31 +++++++++----- .../app/src/lib/crash-invite-store.test.ts | 1 + packages/app/src/locales/en/messages.json | 6 +-- packages/app/src/locales/en/messages.po | 4 +- packages/app/src/locales/pseudo/messages.json | 6 +-- packages/app/src/locales/pseudo/messages.po | 2 +- packages/core/src/logger-types.ts | 11 +++++ .../desktop/src/main/crash-detection.test.ts | 15 +++++++ packages/desktop/src/main/crash-detection.ts | 40 ++++++++++++++----- 12 files changed, 127 insertions(+), 38 deletions(-) create mode 100644 .changeset/crash-invite-includes-minidump.md diff --git a/.changeset/crash-invite-includes-minidump.md b/.changeset/crash-invite-includes-minidump.md new file mode 100644 index 000000000..78fc59c98 --- /dev/null +++ b/.changeset/crash-invite-includes-minidump.md @@ -0,0 +1,9 @@ +--- +"@inkeep/open-knowledge": patch +--- + +When OpenKnowledge invites you to report a crash it just detected, and a crash dump for that crash is on disk, the dump is now included in the report by default. The crash dump is a memory snapshot from the moment of the crash, and the single most useful artifact for finding the cause, so it now rides along unless you uncheck it. Previously it was off by default, so crash reports usually arrived without the very dump that triggered them. + +The crash-dump option now appears only when a dump actually exists for the crash, so a crash invite with nothing to include (for example a session that ended without a clean quit but left no native dump) no longer shows a dead checkbox. + +Nothing about the consent flow changes: the crash dump is still a labeled checkbox you can turn off, the note still says it can contain document content and can't be redacted, and nothing is sent until you review the exact bundle. This only affects reports triggered by a detected crash; regular bug reports are unchanged and never include a crash dump. diff --git a/packages/app/src/components/ReportBugCrashInviteTrigger.dom.test.tsx b/packages/app/src/components/ReportBugCrashInviteTrigger.dom.test.tsx index 56a727e12..457d07e91 100644 --- a/packages/app/src/components/ReportBugCrashInviteTrigger.dom.test.tsx +++ b/packages/app/src/components/ReportBugCrashInviteTrigger.dom.test.tsx @@ -46,6 +46,7 @@ const INVITE: OkBugReportCrashDetectedEvent = { eventId: 'boot:1751871600000', kind: 'boot', context: { dirtyShutdown: true, newMinidumps: 0 }, + minidumpAvailable: false, }; interface CrashBridgeStub { diff --git a/packages/app/src/components/ReportBugDialog.dom.test.tsx b/packages/app/src/components/ReportBugDialog.dom.test.tsx index b4495cd0e..f4139356a 100644 --- a/packages/app/src/components/ReportBugDialog.dom.test.tsx +++ b/packages/app/src/components/ReportBugDialog.dom.test.tsx @@ -523,9 +523,10 @@ describe('ReportBugDialog', () => { eventId: 'boot:1751871600000', kind: 'boot', context: { dirtyShutdown: true, newMinidumps: 1 }, + minidumpAvailable: true, }; - test('a crash invite reskins compose: banner, crash note label, pre-checked diagnostics, off-by-default dump, Not now', async () => { + test('a crash invite reskins compose: banner, crash note label, pre-checked diagnostics, on-by-default dump, Not now', async () => { installBridge(); await renderDialog({ crashInvite: BOOT_INVITE }); @@ -549,8 +550,8 @@ describe('ReportBugDialog', () => { ).toBe('true'); const dumpBox = screen.getByRole('checkbox', { name: 'Crash dump' }); - expect(dumpBox.getAttribute('aria-checked')).toBe('false'); - expect(screen.getByText(/a memory snapshot from the crash\./i)).not.toBeNull(); + expect(dumpBox.getAttribute('aria-checked')).toBe('true'); + expect(screen.getByText(/a memory snapshot from the crash/i)).not.toBeNull(); expect(screen.getByText(/can't be redacted/i)).not.toBeNull(); expect(screen.getByRole('button', { name: 'Not now' })).not.toBeNull(); @@ -560,7 +561,7 @@ describe('ReportBugDialog', () => { expect(screen.queryByText(/secrets like api keys and tokens are redacted/i)).toBeNull(); }); - test('crash-invite create folds the crash details in and leaves the dump out unless opted in', async () => { + test('crash-invite create folds the crash details in and includes the dump by default', async () => { const log = installBridge(); await renderDialog({ crashInvite: BOOT_INVITE }); @@ -571,12 +572,12 @@ describe('ReportBugDialog', () => { { level: 'full', note: 'Crash source: previous session ended without a clean quit\nCrash event: boot:1751871600000', - includeCrashDump: false, + includeCrashDump: true, }, ]); }); - test('checking Crash dump opts the minidump into create', async () => { + test('unchecking Crash dump excludes the minidump from create', async () => { const log = installBridge(); await renderDialog({ crashInvite: BOOT_INVITE }); @@ -584,7 +585,29 @@ describe('ReportBugDialog', () => { await userEvent.click(screen.getByRole('button', { name: 'Create report' })); await screen.findByRole('heading', { name: 'Review your report' }); - expect(log.createCalls[0]?.includeCrashDump).toBe(true); + expect(log.createCalls[0]?.includeCrashDump).toBe(false); + }); + + test('a crash invite with no available minidump shows no dump row and sends no flag', async () => { + const log = installBridge(); + await renderDialog({ + crashInvite: { + eventId: 'boot:1751871600001', + kind: 'boot', + context: { dirtyShutdown: true, newMinidumps: 0 }, + minidumpAvailable: false, + }, + }); + + // A dirty shutdown that left no native crash dump: the invite still opens, + // but there is nothing to include, so no dead checkbox is offered. + expect(screen.queryByRole('checkbox', { name: 'Crash dump' })).toBeNull(); + expect(screen.getByText('OpenKnowledge quit unexpectedly last time.')).not.toBeNull(); + + await userEvent.click(screen.getByRole('button', { name: 'Create report' })); + await screen.findByRole('heading', { name: 'Review your report' }); + + expect(log.createCalls[0]).not.toHaveProperty('includeCrashDump'); }); test('the plain compose never renders the crash-dump opt-in and never sends the flag', async () => { @@ -611,7 +634,7 @@ describe('ReportBugDialog', () => { }); await renderDialog({ crashInvite: BOOT_INVITE }); - await userEvent.click(screen.getByRole('checkbox', { name: 'Crash dump' })); + // The dump rides in by default for a crash invite, so no click is needed. await userEvent.click(screen.getByRole('button', { name: 'Create report' })); await screen.findByRole('heading', { name: 'Review your report' }); diff --git a/packages/app/src/components/ReportBugDialogBody.tsx b/packages/app/src/components/ReportBugDialogBody.tsx index 603499fe5..63109c1fe 100644 --- a/packages/app/src/components/ReportBugDialogBody.tsx +++ b/packages/app/src/components/ReportBugDialogBody.tsx @@ -179,8 +179,9 @@ export interface ReportBugDialogProps { * Present when a crash-detected invitation opened the dialog * (`ReportBugCrashInviteTrigger`). Switches compose to the crash-invite * variant: banner, "What were you doing?" note label, detailed diagnostics - * pre-checked, the crash-dump opt-in row (default off), and a "Not now" - * dismiss. The event's kind and id fold into the report's note. + * pre-checked, the crash-dump row (only when the event's `minidumpAvailable` + * is true; default on, opt-out), and a "Not now" dismiss. The event's kind + * and id fold into the report's note. */ crashInvite?: OkBugReportCrashDetectedEvent; /** @@ -206,7 +207,16 @@ function ReportBugDialog({ const [phase, setPhase] = useState(COMPOSE_IDLE); const [note, setNote] = useState(''); const [detailed, setDetailed] = useState(crashContext !== undefined || crashInvite !== undefined); - const [includeDump, setIncludeDump] = useState(false); + // The crash-dump opt-in only exists when main confirmed a minidump is on + // disk for this event; a dump-less invite (e.g. a dirty shutdown that left + // no native crash) offers no dead checkbox. + const crashDumpAvailable = crashInvite?.minidumpAvailable === true; + // Default ON when a dump is available: the crash is the whole reason for the + // report, and its minidump is the artifact triage most needs. Consent is + // preserved without a silent send — the row stays visible and uncheckable, + // its hint states the memory is unredactable, and the review step flags + // "crash dump not redacted" before the user sends. + const [includeDump, setIncludeDump] = useState(crashDumpAvailable); // Default-on per the spec: when a screenshot was captured it rides along // unless the user unchecks it. Only ever sent to `create` when one exists. const [includeScreenshot, setIncludeScreenshot] = useState(true); @@ -261,7 +271,7 @@ function ReportBugDialog({ if (phase.step === 'success' || phase.step === 'email' || phase.step === 'failure') { setNote(''); setDetailed(crashContext !== undefined || crashInvite !== undefined); - setIncludeDump(false); + setIncludeDump(crashDumpAvailable); // Re-default the screenshot to on so the next open (which captures a // fresh screenshot) starts checked, matching the compose default. setIncludeScreenshot(true); @@ -286,9 +296,9 @@ function ReportBugDialog({ const result = await bugReport.create({ level: detailed ? 'full' : 'standard', note: composeNote(note, noteContextLines), - // Only the crash invite ever asks for the dump — the plain compose has - // no opt-in surface, so it must not even send the flag. - ...(crashInvite !== undefined ? { includeCrashDump: includeDump } : {}), + // Only a crash invite with an available dump exposes the opt-in, so only + // then is the flag sent — plain compose and dump-less invites omit it. + ...(crashDumpAvailable ? { includeCrashDump: includeDump } : {}), // Only send the flag when a screenshot was actually captured — absent // means main has nothing staged, so it must not claim an inclusion. ...(screenshot !== null ? { includeScreenshot } : {}), @@ -561,7 +571,7 @@ function ReportBugDialog({ )} - {crashInvite !== undefined && ( + {crashDumpAvailable && (

- A memory snapshot from the crash. It can contain document content and - can't be redacted, so leave it off unless you're comfortable sharing it. + A memory snapshot from the crash, and the artifact that helps us most. It + can contain document content and can't be redacted, so uncheck it if you'd + rather not share it.

diff --git a/packages/app/src/lib/crash-invite-store.test.ts b/packages/app/src/lib/crash-invite-store.test.ts index e3b3b980c..8c64a4b74 100644 --- a/packages/app/src/lib/crash-invite-store.test.ts +++ b/packages/app/src/lib/crash-invite-store.test.ts @@ -36,6 +36,7 @@ const crashEvent: OkBugReportCrashDetectedEvent = { eventId: 'crash:render:1751871600000:0', kind: 'render-process-gone', context: { reason: 'crashed', exitCode: 5 }, + minidumpAvailable: false, }; describe('createCrashInviteStore — install', () => { diff --git a/packages/app/src/locales/en/messages.json b/packages/app/src/locales/en/messages.json index 88aa8265e..5eb354c76 100644 --- a/packages/app/src/locales/en/messages.json +++ b/packages/app/src/locales/en/messages.json @@ -1539,9 +1539,6 @@ "YEIS9T": ["Edits apply live. Light or dark is chosen from the background."], "YEP3X2": ["Delete a table when the active table cell selection covers the full table."], "YGIi_c": ["Reinstall"], - "YIp4R0": [ - "A memory snapshot from the crash. It can contain document content and can't be redacted, so leave it off unless you're comfortable sharing it." - ], "YJ1BTp": ["A skill with that name already exists."], "YKLP_-": ["Template renamed"], "YLMBSd": ["e.g. The editor froze after I pasted a large table"], @@ -2070,6 +2067,9 @@ "jerqF0": ["Loading branch state"], "jhtG3I": [" Your selection has been restored."], "jjtmV_": ["First file name"], + "jnCROW": [ + "A memory snapshot from the crash, and the artifact that helps us most. It can contain document content and can't be redacted, so uncheck it if you'd rather not share it." + ], "jol95m": ["Failed to open Project Navigator."], "jpctdh": ["View"], "jtg_8-": [ diff --git a/packages/app/src/locales/en/messages.po b/packages/app/src/locales/en/messages.po index 0486384cf..71eb59960 100644 --- a/packages/app/src/locales/en/messages.po +++ b/packages/app/src/locales/en/messages.po @@ -633,8 +633,8 @@ msgid "A line with a footnote<0><1>[1] and a definition shown below." msgstr "A line with a footnote<0><1>[1] and a definition shown below." #: src/components/ReportBugDialogBody.tsx -msgid "A memory snapshot from the crash. It can contain document content and can't be redacted, so leave it off unless you're comfortable sharing it." -msgstr "A memory snapshot from the crash. It can contain document content and can't be redacted, so leave it off unless you're comfortable sharing it." +msgid "A memory snapshot from the crash, and the artifact that helps us most. It can contain document content and can't be redacted, so uncheck it if you'd rather not share it." +msgstr "A memory snapshot from the crash, and the artifact that helps us most. It can contain document content and can't be redacted, so uncheck it if you'd rather not share it." #: src/components/ShareReceiveDialog.tsx msgid "A network problem or GitHub outage interrupted the clone." diff --git a/packages/app/src/locales/pseudo/messages.json b/packages/app/src/locales/pseudo/messages.json index 095caf644..c651ca620 100644 --- a/packages/app/src/locales/pseudo/messages.json +++ b/packages/app/src/locales/pseudo/messages.json @@ -1539,9 +1539,6 @@ "YEIS9T": ["Ēďĩţś àƥƥĺŷ ĺĩvē. Ĺĩĝĥţ ōŕ ďàŕķ ĩś ćĥōśēń ƒŕōḿ ţĥē ƀàćķĝŕōũńď."], "YEP3X2": ["Ďēĺēţē à ţàƀĺē ŵĥēń ţĥē àćţĩvē ţàƀĺē ćēĺĺ śēĺēćţĩōń ćōvēŕś ţĥē ƒũĺĺ ţàƀĺē."], "YGIi_c": ["Ŕēĩńśţàĺĺ"], - "YIp4R0": [ - "À ḿēḿōŕŷ śńàƥśĥōţ ƒŕōḿ ţĥē ćŕàśĥ. Ĩţ ćàń ćōńţàĩń ďōćũḿēńţ ćōńţēńţ àńď ćàń'ţ ƀē ŕēďàćţēď, śō ĺēàvē ĩţ ōƒƒ ũńĺēśś ŷōũ'ŕē ćōḿƒōŕţàƀĺē śĥàŕĩńĝ ĩţ." - ], "YJ1BTp": ["À śķĩĺĺ ŵĩţĥ ţĥàţ ńàḿē àĺŕēàďŷ ēxĩśţś."], "YKLP_-": ["Ţēḿƥĺàţē ŕēńàḿēď"], "YLMBSd": ["ē.ĝ. Ţĥē ēďĩţōŕ ƒŕōźē àƒţēŕ Ĩ ƥàśţēď à ĺàŕĝē ţàƀĺē"], @@ -2070,6 +2067,9 @@ "jerqF0": ["Ĺōàďĩńĝ ƀŕàńćĥ śţàţē"], "jhtG3I": [" Ŷōũŕ śēĺēćţĩōń ĥàś ƀēēń ŕēśţōŕēď."], "jjtmV_": ["Ƒĩŕśţ ƒĩĺē ńàḿē"], + "jnCROW": [ + "À ḿēḿōŕŷ śńàƥśĥōţ ƒŕōḿ ţĥē ćŕàśĥ, àńď ţĥē àŕţĩƒàćţ ţĥàţ ĥēĺƥś ũś ḿōśţ. Ĩţ ćàń ćōńţàĩń ďōćũḿēńţ ćōńţēńţ àńď ćàń'ţ ƀē ŕēďàćţēď, śō ũńćĥēćķ ĩţ ĩƒ ŷōũ'ď ŕàţĥēŕ ńōţ śĥàŕē ĩţ." + ], "jol95m": ["Ƒàĩĺēď ţō ōƥēń ƤŕōĴēćţ Ńàvĩĝàţōŕ."], "jpctdh": ["Vĩēŵ"], "jtg_8-": [ diff --git a/packages/app/src/locales/pseudo/messages.po b/packages/app/src/locales/pseudo/messages.po index f12a24424..59e93a1fa 100644 --- a/packages/app/src/locales/pseudo/messages.po +++ b/packages/app/src/locales/pseudo/messages.po @@ -629,7 +629,7 @@ msgid "A line with a footnote<0><1>[1] and a definition shown below." msgstr "" #: src/components/ReportBugDialogBody.tsx -msgid "A memory snapshot from the crash. It can contain document content and can't be redacted, so leave it off unless you're comfortable sharing it." +msgid "A memory snapshot from the crash, and the artifact that helps us most. It can contain document content and can't be redacted, so uncheck it if you'd rather not share it." msgstr "" #: src/components/ShareReceiveDialog.tsx diff --git a/packages/core/src/logger-types.ts b/packages/core/src/logger-types.ts index 06d5d1c3d..c77560a56 100644 --- a/packages/core/src/logger-types.ts +++ b/packages/core/src/logger-types.ts @@ -168,23 +168,34 @@ export type OkBugReportSendResult = * * Lives in core for the same reason as the sibling bug-report types: the * desktop bridge contract's per-package copies must all name one type. + * + * `minidumpAvailable` is main's authoritative answer (only main can stat the + * crash-dumps dir) to "is there a crash minidump to include for this event?". + * The report dialog renders the crash-dump opt-in only when it is true — an + * invite with no dump (e.g. a dirty shutdown that left none) offers no dead + * checkbox. Advisory for UI gating only: the actual attach still re-checks the + * dump on disk at bundle time, so a dump that vanished by then is simply + * omitted. */ export type OkBugReportCrashDetectedEvent = | { eventId: string; kind: 'render-process-gone'; context: { reason: string; exitCode?: number }; + minidumpAvailable: boolean; } | { eventId: string; kind: 'child-process-gone'; context: { reason: string; processType: string; name?: string; exitCode?: number }; + minidumpAvailable: boolean; } | { eventId: string; /** Boot-time detection: the previous session left a dirty-shutdown sentinel or fresh minidumps. */ kind: 'boot'; context: { dirtyShutdown: boolean; newMinidumps: number }; + minidumpAvailable: boolean; }; /** diff --git a/packages/desktop/src/main/crash-detection.test.ts b/packages/desktop/src/main/crash-detection.test.ts index c136cf2e5..0f5b28e4f 100644 --- a/packages/desktop/src/main/crash-detection.test.ts +++ b/packages/desktop/src/main/crash-detection.test.ts @@ -105,6 +105,18 @@ describe('runtime process-gone invitations', () => { expect(event.context.reason).toBe('crashed'); expect(event.context.exitCode).toBe(5); } + // No dump on disk for this crash, so the invite offers no dump option. + expect(event?.minidumpAvailable).toBe(false); + }); + + test('a renderer crash with a fresh minidump on disk reports it as available', () => { + const rig = makeRig(); + const detection = createCrashDetection(rig.deps); + seedMinidump(rig, 'completed/renderer.dmp', rig.tick()); + + detection.handleRenderProcessGone({ reason: 'crashed' }); + + expect(rig.emitted[0]?.minidumpAvailable).toBe(true); }); test('routine process teardown never invites', () => { @@ -182,6 +194,8 @@ describe('boot-time detection', () => { if (armed?.kind === 'boot') { expect(armed.context.dirtyShutdown).toBe(true); expect(armed.context.newMinidumps).toBe(0); + // A dirty shutdown that left no native dump offers no dump option. + expect(armed.minidumpAvailable).toBe(false); } // Boot events wait for the first ready renderer instead of racing window load. @@ -246,6 +260,7 @@ describe('boot-time detection', () => { if (armed?.kind === 'boot') { expect(armed.context.dirtyShutdown).toBe(false); expect(armed.context.newMinidumps).toBe(1); + expect(armed.minidumpAvailable).toBe(true); } if (!armed) throw new Error('expected a minidump-driven boot invitation'); sessionB.ack(armed.eventId); diff --git a/packages/desktop/src/main/crash-detection.ts b/packages/desktop/src/main/crash-detection.ts index c05d6b5be..19dd79105 100644 --- a/packages/desktop/src/main/crash-detection.ts +++ b/packages/desktop/src/main/crash-detection.ts @@ -113,8 +113,9 @@ export interface CrashDetection { * crash the user is currently invited to report. Null when the un-acked * crash left no dump (e.g. dirty shutdown without a native crash) or every * dump is already acked. Minidumps carry raw process memory that text - * redaction cannot scrub, so bundle inclusion must stay behind the report - * dialog's explicit opt-in that calls this. + * redaction cannot scrub, so bundle inclusion stays behind the report + * dialog's crash-dump checkbox (pre-checked for a crash invite, opt-out) + * plus the review-before-send step that calls this — never a silent attach. */ newestMinidumpPath(): string | null; } @@ -250,6 +251,23 @@ export function createCrashDetection(deps: CrashDetectionDeps): CrashDetection { return true; } + /** + * Newest minidump strictly newer than the ack baseline, or null. Shared by + * the report-time path lookup and the per-event availability signal so both + * answer from the same baseline-filtered scan of the crash-dumps dir. + */ + function newestMinidumpEntry(): MinidumpEntry | null { + const entries: MinidumpEntry[] = []; + collectMinidumpEntries(deps.crashDumpsDir, MINIDUMP_SCAN_DEPTH, entries); + const baselineMs = Date.parse(store.minidumpBaselineAt); + let newest: MinidumpEntry | null = null; + for (const entry of entries) { + if (entry.mtimeMs <= baselineMs) continue; + if (newest === null || entry.mtimeMs > newest.mtimeMs) newest = entry; + } + return newest; + } + return { detectBootCrash(): OkBugReportCrashDetectedEvent | null { const detectedAt = deps.now(); @@ -294,6 +312,9 @@ export function createCrashDetection(deps: CrashDetectionDeps): CrashDetection { eventId, kind: 'boot', context: { dirtyShutdown: sentinelPresent, newMinidumps: newDumps.length }, + // A prior-session dump is already on disk, so the freshness scan + // that produced newDumps is the authoritative availability answer. + minidumpAvailable: newDumps.length > 0, }; if (armInvite(event)) { armed = event; @@ -366,6 +387,10 @@ export function createCrashDetection(deps: CrashDetectionDeps): CrashDetection { reason: details.reason, ...(details.exitCode !== undefined ? { exitCode: details.exitCode } : {}), }, + // Best-effort: Crashpad may still be flushing the dump when this + // signal fires. A dump that lands just after reads as unavailable + // here (no checkbox); the boot-time path is the reliable one. + minidumpAvailable: newestMinidumpEntry() !== null, }) ) { tryDeliver(); @@ -393,6 +418,7 @@ export function createCrashDetection(deps: CrashDetectionDeps): CrashDetection { ...(details.name !== undefined ? { name: details.name } : {}), ...(details.exitCode !== undefined ? { exitCode: details.exitCode } : {}), }, + minidumpAvailable: newestMinidumpEntry() !== null, }) ) { tryDeliver(); @@ -420,15 +446,7 @@ export function createCrashDetection(deps: CrashDetectionDeps): CrashDetection { }, newestMinidumpPath(): string | null { - const entries: MinidumpEntry[] = []; - collectMinidumpEntries(deps.crashDumpsDir, MINIDUMP_SCAN_DEPTH, entries); - const baselineMs = Date.parse(store.minidumpBaselineAt); - let newest: MinidumpEntry | null = null; - for (const entry of entries) { - if (entry.mtimeMs <= baselineMs) continue; - if (newest === null || entry.mtimeMs > newest.mtimeMs) newest = entry; - } - return newest === null ? null : newest.path; + return newestMinidumpEntry()?.path ?? null; }, }; }