Skip to content

Commit 33dac8e

Browse files
committed
update issue format
1 parent 6124f0d commit 33dac8e

2 files changed

Lines changed: 168 additions & 379 deletions

File tree

workers/task-manager/src/utils/issue.ts

Lines changed: 168 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,71 @@ import type { GroupedEventDBScheme, ProjectDBScheme } from '@hawk.so/types';
22
import { decodeUnsafeFields } from '../../../../lib/utils/unsafeFields';
33
import type { IssueData } from '../GithubService';
44

5+
/**
6+
* Format date for display in GitHub issue
7+
*
8+
* @param timestamp - Unix timestamp in seconds
9+
* @returns Formatted date string (e.g., "23 Feb 2025 14:40:21")
10+
*/
11+
function formatDate(timestamp: number): string {
12+
const date = new Date(timestamp * 1000);
13+
const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ];
14+
const day = date.getUTCDate();
15+
const month = months[date.getUTCMonth()];
16+
const year = date.getUTCFullYear();
17+
const hours = date.getUTCHours().toString().padStart(2, '0');
18+
const minutes = date.getUTCMinutes().toString().padStart(2, '0');
19+
const seconds = date.getUTCSeconds().toString().padStart(2, '0');
20+
21+
return `${day} ${month} ${year} ${hours}:${minutes}:${seconds}`;
22+
}
23+
24+
/**
25+
* Calculate days repeating from timestamp
26+
*
27+
* @param timestamp - Unix timestamp in seconds
28+
* @returns Number of days since first occurrence
29+
*/
30+
function calculateDaysRepeating(timestamp: number): number {
31+
const now = Date.now();
32+
const eventTimestamp = timestamp * 1000;
33+
const differenceInDays = (now - eventTimestamp) / (1000 * 3600 * 24);
34+
35+
return Math.round(differenceInDays);
36+
}
37+
38+
/**
39+
* Format source code as diff with line numbers
40+
* The error line is marked with minus sign, other lines with space
41+
*
42+
* @param sourceCode - Array of source code lines
43+
* @param errorLine - Line number where error occurred
44+
* @returns Formatted diff string
45+
*/
46+
function formatSourceCodeAsDiff(sourceCode: Array<{ line: number; content: string }>, errorLine: number): string {
47+
const lines: string[] = [];
48+
49+
for (const sourceLine of sourceCode) {
50+
const lineNumber = sourceLine.line.toString().padStart(3, ' ');
51+
const isErrorLine = sourceLine.line === errorLine;
52+
const prefix = isErrorLine ? '-' : ' ';
53+
54+
/**
55+
* Escape HTML entities in content
56+
*/
57+
const escapedContent = sourceLine.content
58+
.replace(/&/g, '&amp;')
59+
.replace(/</g, '&lt;')
60+
.replace(/>/g, '&gt;')
61+
.replace(/"/g, '&quot;')
62+
.replace(/'/g, '&#39;');
63+
64+
lines.push(`${prefix}${lineNumber}: ${escapedContent}`);
65+
}
66+
67+
return lines.join('\n');
68+
}
69+
570
/**
671
* Format GitHub Issue from event
772
*
@@ -27,73 +92,136 @@ export function formatIssueFromEvent(event: GroupedEventDBScheme, project: Proje
2792
const title = `[Hawk] ${decodedEvent.payload.title}`;
2893

2994
/**
30-
* Format body with:
31-
* - Link to event page in Hawk
32-
* - totalCount
33-
* - Stacktrace (top frames, truncated)
34-
* - Technical marker: hawk_groupHash
95+
* Format body according to the template:
96+
* - H2 header
97+
* - Stacktrace with first frame expanded, others in details
98+
* - Table with event data
99+
* - Context and Addons as JSON
100+
* - Link to event in Hawk
35101
*/
36102
const bodyParts: string[] = [];
37103

38104
/**
39-
* Link to event page
40-
*/
41-
bodyParts.push(`**View in Hawk:** ${eventUrl}`);
42-
43-
/**
44-
* Total count
105+
* H2 header with title
45106
*/
46-
bodyParts.push(`\n**Total occurrences:** ${decodedEvent.totalCount}`);
107+
bodyParts.push(`## ${decodedEvent.payload.title}`);
47108

48109
/**
49-
* Stacktrace (top frames, truncated to 10 frames max)
110+
* Stacktrace section
50111
*/
51112
if (decodedEvent.payload.backtrace && decodedEvent.payload.backtrace.length > 0) {
52-
bodyParts.push('\n**Stacktrace:**');
53-
bodyParts.push('```');
113+
const firstFrame = decodedEvent.payload.backtrace[0];
114+
const file = firstFrame.file || '<unknown>';
115+
const line = firstFrame.line || 0;
116+
const column = firstFrame.column || 0;
117+
const func = firstFrame.function || '<anonymous>';
54118

55119
/**
56-
* Maximum number of frames to show in issue
120+
* First frame - always visible
57121
*/
58-
const MAX_FRAMES_TO_SHOW = 10;
122+
bodyParts.push(`\n- at ${func} (${file}:${line}:${column})`);
59123

60124
/**
61-
* Take top frames and format them
125+
* Source code for first frame in diff format
62126
*/
63-
const topFrames = decodedEvent.payload.backtrace.slice(0, MAX_FRAMES_TO_SHOW);
127+
if (firstFrame.sourceCode && firstFrame.sourceCode.length > 0) {
128+
bodyParts.push('\n```diff');
129+
bodyParts.push(formatSourceCodeAsDiff(firstFrame.sourceCode, line));
130+
bodyParts.push('```');
131+
}
64132

65-
for (const frame of topFrames) {
66-
const file = frame.file || '<unknown>';
67-
const line = frame.line || 0;
68-
const column = frame.column || 0;
69-
const func = frame.function || '<anonymous>';
133+
/**
134+
* Additional frames in details section
135+
*/
136+
if (decodedEvent.payload.backtrace.length > 1) {
137+
bodyParts.push('\n<details>');
138+
bodyParts.push(' <summary>View full stack trace</summary>');
139+
bodyParts.push(' \n');
140+
141+
for (let i = 1; i < decodedEvent.payload.backtrace.length; i++) {
142+
const frame = decodedEvent.payload.backtrace[i];
143+
const frameFile = frame.file || '<unknown>';
144+
const frameLine = frame.line || 0;
145+
const frameColumn = frame.column || 0;
146+
const frameFunc = frame.function || '<anonymous>';
147+
148+
bodyParts.push(`- at ${frameFunc} (${frameFile}:${frameLine}:${frameColumn})`);
149+
150+
/**
151+
* Source code for this frame in diff format
152+
*/
153+
if (frame.sourceCode && frame.sourceCode.length > 0) {
154+
bodyParts.push('\n```diff');
155+
bodyParts.push(formatSourceCodeAsDiff(frame.sourceCode, frameLine));
156+
bodyParts.push('```');
157+
}
70158

71-
bodyParts.push(`at ${func} (${file}:${line}:${column})`);
159+
/**
160+
* Add newline between frames if not last
161+
*/
162+
if (i < decodedEvent.payload.backtrace.length - 1) {
163+
bodyParts.push('');
164+
}
165+
}
72166

73-
/**
74-
* Maximum number of source code lines to show per frame
75-
*/
76-
const MAX_SOURCE_LINES_PER_FRAME = 3;
167+
bodyParts.push('\n</details>');
168+
}
169+
}
170+
171+
/**
172+
* Table with event data
173+
*/
174+
const sinceDate = formatDate(decodedEvent.timestamp);
175+
const daysRepeating = calculateDaysRepeating(decodedEvent.timestamp);
77176

78-
/**
79-
* Add source code snippet if available (first 3 lines)
80-
*/
81-
if (frame.sourceCode && frame.sourceCode.length > 0) {
82-
const sourceLines = frame.sourceCode.slice(0, MAX_SOURCE_LINES_PER_FRAME);
177+
bodyParts.push('\n| Param | Value |');
178+
bodyParts.push('| -- | :--: |');
179+
bodyParts.push(`| Since | ${sinceDate} |`);
180+
bodyParts.push(`| Days Repeating | ${daysRepeating} |`);
181+
bodyParts.push(`| Total Occurrences | ${decodedEvent.totalCount} |`);
182+
bodyParts.push(`| Users Affected | ${decodedEvent.usersAffected || '-'} |`);
83183

84-
for (const sourceLine of sourceLines) {
85-
bodyParts.push(` ${sourceLine.line}: ${sourceLine.content}`);
86-
}
87-
}
184+
/**
185+
* Context and Addons sections in details
186+
*/
187+
if (decodedEvent.payload.context || decodedEvent.payload.addons) {
188+
bodyParts.push('\n<details>');
189+
bodyParts.push(' <summary>View Context and Addons</summary>');
190+
bodyParts.push(' \n');
191+
192+
/**
193+
* Context section
194+
*/
195+
if (decodedEvent.payload.context) {
196+
bodyParts.push('### Context');
197+
bodyParts.push('\n```json');
198+
bodyParts.push(JSON.stringify(decodedEvent.payload.context, null, 2));
199+
bodyParts.push('```');
200+
}
201+
202+
/**
203+
* Addons section
204+
*/
205+
if (decodedEvent.payload.addons) {
206+
bodyParts.push('\n### Addons');
207+
bodyParts.push('\n```json');
208+
bodyParts.push(JSON.stringify(decodedEvent.payload.addons, null, 2));
209+
bodyParts.push('```');
88210
}
89211

90-
bodyParts.push('```');
212+
bodyParts.push('\n</details>');
91213
}
92214

215+
/**
216+
* Link to event in Hawk
217+
*/
218+
bodyParts.push('\n### Details');
219+
bodyParts.push(`\n[View in Hawk](${eventUrl})`);
220+
93221
/**
94222
* Technical marker for tracking
95223
*/
96-
bodyParts.push(`\n<!-- hawk_groupHash: ${event.groupHash} -->`);
224+
bodyParts.push(`\n\n<!-- hawk_groupHash: ${event.groupHash} -->`);
97225

98226
const body = bodyParts.join('\n');
99227

0 commit comments

Comments
 (0)