Skip to content

Commit f974322

Browse files
committed
fix: improve crash report selection algorithm
1 parent 6765859 commit f974322

3 files changed

Lines changed: 179 additions & 113 deletions

File tree

packages/platform-ios/src/__tests__/simctl.test.ts

Lines changed: 85 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -10,72 +10,57 @@ describe('simctl collectCrashReports', () => {
1010
vi.restoreAllMocks();
1111
});
1212

13-
it('extracts matching simulator .ips crash reports', async () => {
13+
it('extracts matching simulator .ips crash reports by filename prefix', async () => {
1414
const diagnosticReportsDir = join(
1515
homedir(),
1616
'Library',
1717
'Logs',
1818
'DiagnosticReports'
1919
);
2020
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
21+
// OtherApp file is present but must be ignored purely based on filename prefix
2122
vi.spyOn(fs, 'readdirSync').mockReturnValue([
2223
'HarnessPlayground-2026-03-12-122756.ips',
2324
'OtherApp-2026-03-12-122756.ips',
2425
] as unknown as ReturnType<typeof fs.readdirSync>);
25-
vi.spyOn(fs, 'readFileSync').mockImplementation(((path: fs.PathOrFileDescriptor) => {
26-
const filePath = String(path);
27-
28-
if (filePath.includes('HarnessPlayground')) {
29-
return [
30-
JSON.stringify({
31-
app_name: 'HarnessPlayground',
32-
bundleID: 'com.harnessplayground',
33-
name: 'HarnessPlayground',
34-
}),
35-
JSON.stringify({
36-
pid: 1234,
37-
procName: 'HarnessPlayground',
38-
faultingThread: 0,
39-
threads: [
40-
{
41-
frames: [
42-
{
43-
symbol: '_assertionFailure(_:_:file:line:flags:)',
44-
symbolLocation: 156,
45-
imageIndex: 1,
46-
},
47-
{
48-
symbol: 'AppDelegate.crashIfRequested()',
49-
sourceFile: 'AppDelegate.swift',
50-
sourceLine: 31,
51-
imageIndex: 1,
52-
},
53-
],
54-
},
55-
],
56-
usedImages: [{ name: 'dyld' }, { name: 'HarnessPlayground' }],
57-
procPath:
58-
`${homedir()}/Library/Developer/CoreSimulator/Devices/sim-udid/data/Containers/Bundle/Application/ABC/HarnessPlayground.app/HarnessPlayground`,
59-
exception: {
60-
type: 'EXC_BREAKPOINT',
61-
signal: 'SIGTRAP',
62-
},
63-
}),
64-
].join('\n');
65-
}
66-
67-
return [
26+
vi.spyOn(fs, 'readFileSync').mockReturnValue(
27+
[
6828
JSON.stringify({
69-
app_name: 'OtherApp',
70-
bundleID: 'com.other.app',
29+
app_name: 'HarnessPlayground',
30+
bundleID: 'com.harnessplayground',
31+
name: 'HarnessPlayground',
7132
}),
7233
JSON.stringify({
73-
procName: 'OtherApp',
34+
pid: 1234,
35+
procName: 'HarnessPlayground',
7436
procPath:
75-
`${homedir()}/Library/Developer/CoreSimulator/Devices/other-udid/data/Containers/Bundle/Application/DEF/OtherApp.app/OtherApp`,
37+
`${homedir()}/Library/Developer/CoreSimulator/Devices/sim-udid/data/Containers/Bundle/Application/ABC/HarnessPlayground.app/HarnessPlayground`,
38+
faultingThread: 0,
39+
threads: [
40+
{
41+
frames: [
42+
{
43+
symbol: '_assertionFailure(_:_:file:line:flags:)',
44+
symbolLocation: 156,
45+
imageIndex: 1,
46+
},
47+
{
48+
symbol: 'AppDelegate.crashIfRequested()',
49+
sourceFile: 'AppDelegate.swift',
50+
sourceLine: 31,
51+
imageIndex: 1,
52+
},
53+
],
54+
},
55+
],
56+
usedImages: [{ name: 'dyld' }, { name: 'HarnessPlayground' }],
57+
exception: {
58+
type: 'EXC_BREAKPOINT',
59+
signal: 'SIGTRAP',
60+
},
7661
}),
77-
].join('\n');
78-
}) as typeof fs.readFileSync);
62+
].join('\n') as ReturnType<typeof fs.readFileSync>
63+
);
7964
vi.spyOn(fs, 'statSync').mockReturnValue({
8065
mtimeMs: 123456,
8166
} as fs.Stats);
@@ -170,8 +155,8 @@ describe('simctl collectCrashReports', () => {
170155
it('ignores simulator reports older than the current run window', async () => {
171156
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
172157
vi.spyOn(fs, 'readdirSync').mockReturnValue([
173-
'old.ips',
174-
'new.ips',
158+
'HarnessPlayground-2026-03-12-113008.ips',
159+
'HarnessPlayground-2026-03-12-114008.ips',
175160
] as unknown as ReturnType<typeof fs.readdirSync>);
176161
vi.spyOn(fs, 'readFileSync').mockImplementation(((input: fs.PathOrFileDescriptor) => {
177162
const filePath = String(input);
@@ -183,7 +168,7 @@ describe('simctl collectCrashReports', () => {
183168
name: 'HarnessPlayground',
184169
}),
185170
JSON.stringify({
186-
pid: filePath.includes('old') ? 1234 : 1235,
171+
pid: filePath.includes('113008') ? 1234 : 1235,
187172
procName: 'HarnessPlayground',
188173
procPath:
189174
`${homedir()}/Library/Developer/CoreSimulator/Devices/sim-udid/data/Containers/Bundle/Application/ABC/HarnessPlayground.app/HarnessPlayground`,
@@ -195,7 +180,7 @@ describe('simctl collectCrashReports', () => {
195180
].join('\n');
196181
}) as typeof fs.readFileSync);
197182
vi.spyOn(fs, 'statSync').mockImplementation(((input: fs.PathLike) => ({
198-
mtimeMs: String(input).includes('old')
183+
mtimeMs: String(input).includes('113008')
199184
? Date.parse('2026-03-12T11:30:08.000Z')
200185
: Date.parse('2026-03-12T11:40:08.000Z'),
201186
})) as typeof fs.statSync);
@@ -210,4 +195,50 @@ describe('simctl collectCrashReports', () => {
210195
expect(reports).toHaveLength(1);
211196
expect(reports[0]?.pid).toBe(1235);
212197
});
198+
199+
it('returns the latest crash report that matches the simulator udid, skipping newer ones from other simulators', async () => {
200+
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
201+
vi.spyOn(fs, 'readdirSync').mockReturnValue([
202+
'HarnessPlayground-2026-03-12-110000.ips',
203+
'HarnessPlayground-2026-03-12-120000.ips',
204+
'HarnessPlayground-2026-03-12-130000.ips',
205+
] as unknown as ReturnType<typeof fs.readdirSync>);
206+
vi.spyOn(fs, 'readFileSync').mockImplementation(((input: fs.PathOrFileDescriptor) => {
207+
const filePath = String(input);
208+
// The newest file (130000) belongs to a different simulator; the second-newest (120000) is ours
209+
const udid = filePath.includes('130000') ? 'other-sim-udid' : 'sim-udid';
210+
const pid = filePath.includes('110000') ? 1001 : filePath.includes('120000') ? 1002 : 1003;
211+
212+
return [
213+
JSON.stringify({ app_name: 'HarnessPlayground', bundleID: 'com.harnessplayground' }),
214+
JSON.stringify({
215+
pid,
216+
procName: 'HarnessPlayground',
217+
procPath:
218+
`${homedir()}/Library/Developer/CoreSimulator/Devices/${udid}/data/Containers/Bundle/Application/ABC/HarnessPlayground.app/HarnessPlayground`,
219+
exception: { type: 'EXC_BREAKPOINT', signal: 'SIGTRAP' },
220+
}),
221+
].join('\n');
222+
}) as typeof fs.readFileSync);
223+
vi.spyOn(fs, 'statSync').mockImplementation(((input: fs.PathLike) => {
224+
const filePath = String(input);
225+
const mtimeMs = filePath.includes('110000')
226+
? Date.parse('2026-03-12T11:00:00.000Z')
227+
: filePath.includes('120000')
228+
? Date.parse('2026-03-12T12:00:00.000Z')
229+
: Date.parse('2026-03-12T13:00:00.000Z');
230+
231+
return { mtimeMs } as fs.Stats;
232+
}) as typeof fs.statSync);
233+
234+
const reports = await collectCrashReports({
235+
udid: 'sim-udid',
236+
bundleId: 'com.harnessplayground',
237+
processNames: ['HarnessPlayground'],
238+
});
239+
240+
expect(reports).toHaveLength(1);
241+
// Skips the newest (pid 1003, other simulator) and returns the second-newest that matches
242+
expect(reports[0]?.pid).toBe(1002);
243+
});
213244
});

packages/platform-ios/src/app-monitor.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,8 +400,12 @@ export const createIosSimulatorAppMonitor = ({
400400
): Promise<AppCrashDetails | null> => {
401401
let fallbackArtifact: AppCrashDetails | null = null;
402402
const deadline = Date.now() + CRASH_ARTIFACT_WAIT_TIMEOUT_MS;
403+
let pollCount = 0;
403404

404405
do {
406+
pollCount += 1;
407+
logger.debug(`[app-monitor] waitForCrashArtifact poll #${pollCount}`, { pid: options.pid, processName: options.processName });
408+
405409
const collectedArtifacts = await simctl.collectCrashReports({
406410
udid,
407411
bundleId,
@@ -410,21 +414,28 @@ export const createIosSimulatorAppMonitor = ({
410414
minOccurredAt: monitorStartedAt,
411415
});
412416

417+
logger.debug(`[app-monitor] poll #${pollCount}: collected ${collectedArtifacts.length} crash artifact(s) from DiagnosticReports`);
418+
413419
for (const artifact of collectedArtifacts) {
414420
base.recordCrashArtifact(artifact);
415421
}
416422

417423
const artifact = base.getLatestCrashArtifact(options);
418424

419425
if (artifact) {
426+
logger.debug(`[app-monitor] poll #${pollCount}: found artifact`, { artifactType: artifact.artifactType, artifactPath: artifact.artifactPath, pid: artifact.pid, processName: artifact.processName });
427+
420428
if (artifact.artifactType === 'ios-crash-report') {
421429
return artifact;
422430
}
423431

424432
fallbackArtifact = artifact;
433+
} else {
434+
logger.debug(`[app-monitor] poll #${pollCount}: no matching artifact yet`);
425435
}
426436

427437
if (Date.now() >= deadline) {
438+
logger.debug(`[app-monitor] waitForCrashArtifact deadline reached, returning ${fallbackArtifact ? 'fallback log-based artifact' : 'null'}`);
428439
return fallbackArtifact;
429440
}
430441

@@ -438,17 +449,20 @@ export const createIosSimulatorAppMonitor = ({
438449
startLogMonitor,
439450
stopLogMonitor,
440451
getCrashDetails: async (options) => {
452+
logger.debug('[app-monitor] getCrashDetails called (simulator)', { pid: options.pid, processName: options.processName });
441453
await new Promise((resolve) =>
442454
setTimeout(resolve, CRASH_ARTIFACT_SETTLE_DELAY_MS)
443455
);
444456

445457
const artifact = await waitForCrashArtifact(options);
446458

447459
if (!artifact) {
460+
logger.debug('[app-monitor] getCrashDetails: no artifact found, returning null');
448461
return null;
449462
}
450463

451464
if (artifact.artifactType === 'ios-crash-report') {
465+
logger.debug('[app-monitor] getCrashDetails: returning ios-crash-report artifact', { artifactPath: artifact.artifactPath });
452466
return artifact;
453467
}
454468

@@ -457,6 +471,8 @@ export const createIosSimulatorAppMonitor = ({
457471
occurredAt: options.occurredAt,
458472
});
459473

474+
logger.debug(`[app-monitor] getCrashDetails: returning log-based artifact (${relatedLogLines.length} related log lines)`);
475+
460476
return {
461477
...artifact,
462478
summary:

0 commit comments

Comments
 (0)