Skip to content

Commit 69ed6e7

Browse files
authored
fix: deduplicate culprit frames (#16)
1 parent e1a7f6c commit 69ed6e7

2 files changed

Lines changed: 97 additions & 2 deletions

File tree

‎src/stParser.ts‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ function parseJsonReport(entry: unknown): StReport | undefined {
172172
timestamp,
173173
headline,
174174
culprit,
175-
frames,
175+
frames: framesWithoutDuplicateCulprit(culprit, frames),
176176
block: JSON.stringify(entry, null, 2) + "\n",
177177
};
178178
}
@@ -200,6 +200,23 @@ function parseFrame(text: string): StFrame | undefined {
200200
return line > 0 ? { file: match[1], line, text: text.trim() } : undefined;
201201
}
202202

203+
function framesWithoutDuplicateCulprit(
204+
culprit: StFrame | undefined,
205+
frames: StFrame[],
206+
): StFrame[] {
207+
if (!culprit) {
208+
return frames;
209+
}
210+
211+
return [
212+
culprit,
213+
...frames.filter(
214+
(frame) =>
215+
frame.file !== culprit.file || frame.line !== culprit.line,
216+
),
217+
];
218+
}
219+
203220
function isObject(value: unknown): value is Record<string, unknown> {
204221
return typeof value === "object" && value !== null && !Array.isArray(value);
205222
}
@@ -245,7 +262,15 @@ function parseBlock(block: string[]): StReport | undefined {
245262
if (!culprit && !markedFrameSeen && frames.length > 0) {
246263
culprit = frames[0];
247264
}
248-
return { id, timestamp, headline, culprit, frames, block: block.join("\n") };
265+
266+
return {
267+
id,
268+
timestamp,
269+
headline,
270+
culprit,
271+
frames: framesWithoutDuplicateCulprit(culprit, frames),
272+
block: block.join("\n"),
273+
};
249274
}
250275

251276
function stripCr(s: string): string {

‎src/test/parser.test.ts‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,73 @@ test("parses unicode filenames and .kts frames", () => {
175175
assert.equal(reports[1].culprit?.file, "構建.kts");
176176
assert.equal(reports[1].culprit?.line, 9);
177177
});
178+
179+
test("deduplicates the culprit location in text and st-json/1 reports", () => {
180+
const textContent = [
181+
"━━━ ERROR #dedupe-text ━━━ 2026-07-10 20:24:00.000 thread=main ━━━",
182+
"IllegalStateException: payment gateway refused",
183+
"at PaymentService.charge(PaymentService.java:118) ← YOUR CODE",
184+
"stack (distilled, 1 of 1 frames):",
185+
" PaymentService.charge(PaymentService.java:118) ← culprit",
186+
"━━━ END #dedupe-text ━━━",
187+
].join("\n");
188+
189+
const jsonContent = JSON.stringify({
190+
type: "report",
191+
id: "dedupe-json",
192+
ts: "2026-07-10T20:24:00.000Z",
193+
error: {
194+
type: "IllegalStateException",
195+
message: "payment gateway refused",
196+
culprit: {
197+
frame: "PaymentService.charge(PaymentService.java:118)",
198+
appCode: true,
199+
},
200+
},
201+
stack: {
202+
shown: 1,
203+
total: 1,
204+
frames: ["PaymentService.charge(PaymentService.java:118)"],
205+
},
206+
});
207+
208+
const textReport = parseReports(textContent)[0];
209+
const jsonReport = parseReports(jsonContent)[0];
210+
211+
const textLocations = textReport.frames.map(
212+
(frame) => `${frame.file}:${frame.line}`,
213+
);
214+
const jsonLocations = jsonReport.frames.map(
215+
(frame) => `${frame.file}:${frame.line}`,
216+
);
217+
218+
assert.deepEqual(textLocations, ["PaymentService.java:118"]);
219+
assert.deepEqual(jsonLocations, textLocations);
220+
assert.match(textReport.frames[0].text, /← YOUR CODE/);
221+
assert.match(jsonReport.frames[0].text, /← YOUR CODE/);
222+
});
223+
224+
test("keeps distinct frames when the culprit is not the top stack frame", () => {
225+
const content = [
226+
"━━━ ERROR #non-top ━━━ 2026-07-10 20:25:00.000 thread=main ━━━",
227+
"IllegalStateException: payment gateway refused",
228+
"at PaymentService.charge(PaymentService.java:118) ← YOUR CODE",
229+
"stack (distilled, 3 of 3 frames):",
230+
" CheckoutController.submit(CheckoutController.java:31)",
231+
" PaymentService.charge(PaymentService.java:118) ← culprit",
232+
" Worker.run(Worker.java:9)",
233+
"━━━ END #non-top ━━━",
234+
].join("\n");
235+
236+
const report = parseReports(content)[0];
237+
238+
assert.deepEqual(
239+
report.frames.map((frame) => `${frame.file}:${frame.line}`),
240+
[
241+
"PaymentService.java:118",
242+
"CheckoutController.java:31",
243+
"Worker.java:9",
244+
],
245+
);
246+
assert.match(report.frames[0].text, /← YOUR CODE/);
247+
});

0 commit comments

Comments
 (0)