Skip to content

Commit 3690d3b

Browse files
authored
Merge pull request #201 from codedogQBY/fix/feedback-log-too-large
fix(feedback): cap log bytes UTF-8-aware so feedback no longer 413s
2 parents 88ade41 + 91621e2 commit 3690d3b

2 files changed

Lines changed: 46 additions & 5 deletions

File tree

packages/core/src/feedback/feedback-service.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ const CONSOLE_LEVELS = ["debug", "info", "log", "warn", "error"] as const;
3232
const LOG_DIR = "logs";
3333
const LOG_FLUSH_INTERVAL_MS = 3000; // Flush to file every 3 seconds
3434
const LOG_MAX_DAYS = 7; // Keep 7 days of logs
35+
/**
36+
* Tail-truncate the collected logs to this UTF-8 byte budget before
37+
* submission. Sits below the worker's MAX_BODY_BYTES with room for the
38+
* description (≤12_000 chars) + device info + JSON envelope, so a payload
39+
* with maxed-out description never trips the worker's 413.
40+
*/
41+
const MAX_LOG_BYTES = 60_000;
3542

3643
/** In-memory write buffer — flushed to file periodically */
3744
let _pendingLines: string[] = [];
@@ -217,6 +224,20 @@ function sanitizeLogLine(line: string): string {
217224
return line;
218225
}
219226

227+
/**
228+
* Tail-truncate a string to at most `maxBytes` UTF-8 bytes. Diagnostic logs
229+
* are most useful at the END — we drop the older lines when oversize. Skips
230+
* leading UTF-8 continuation bytes so the cut lands on a code-point boundary.
231+
*/
232+
export function truncateUtf8Tail(text: string, maxBytes: number): string {
233+
const bytes = new TextEncoder().encode(text);
234+
if (bytes.byteLength <= maxBytes) return text;
235+
let offset = bytes.byteLength - maxBytes;
236+
while (offset < bytes.byteLength && (bytes[offset] & 0xc0) === 0x80) offset++;
237+
const dropped = offset;
238+
return `[truncated: dropped ${dropped} bytes of older logs]\n${new TextDecoder().decode(bytes.slice(offset))}`;
239+
}
240+
220241
/** Collect logs for feedback submission. Reads today's + yesterday's log files. */
221242
export async function collectLogs(options?: { sinceMs?: number }): Promise<string> {
222243
// First flush any pending lines
@@ -260,8 +281,10 @@ export async function collectLogs(options?: { sinceMs?: number }): Promise<strin
260281
return match[1] >= sinceTime;
261282
});
262283

263-
// Sanitize sensitive data before exposing logs externally
264-
return lines.map(sanitizeLogLine).join("\n");
284+
// Sanitize sensitive data before exposing logs externally, then tail-truncate
285+
// to stay under the worker's MAX_BODY_BYTES (most recent lines are most relevant).
286+
const sanitized = lines.map(sanitizeLogLine).join("\n");
287+
return truncateUtf8Tail(sanitized, MAX_LOG_BYTES);
265288
}
266289

267290
/** Clear all log files */

packages/feedback-worker/src/index.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,13 @@ interface GitHubCommentResponse {
5050

5151
const MAX_TITLE_LENGTH = 120;
5252
const MAX_DESCRIPTION_LENGTH = 12_000;
53-
const MAX_LOG_LENGTH = 50_000;
54-
const MAX_BODY_BYTES = 80_000;
53+
/**
54+
* Server-side safety net for logs: byte-based and tail-keep (newest lines
55+
* matter most for diagnostics). Sized above the client's MAX_LOG_BYTES so
56+
* an updated client never gets re-truncated here; old clients still cap.
57+
*/
58+
const MAX_LOG_BYTES = 80_000;
59+
const MAX_BODY_BYTES = 128_000;
5560
const DEFAULT_SUBMISSIONS_PER_DAY = 20;
5661
const DEFAULT_STATUS_REQUESTS_PER_HOUR = 120;
5762

@@ -104,7 +109,7 @@ async function handleCreateFeedback(request: Request, env: Env): Promise<Respons
104109
const type = payload.type ?? "other";
105110
const title = requireText(payload.title, "title", MAX_TITLE_LENGTH);
106111
const description = requireText(payload.description, "description", MAX_DESCRIPTION_LENGTH);
107-
const logs = truncateText(payload.logs?.trim() ?? "", MAX_LOG_LENGTH);
112+
const logs = truncateLogTail(payload.logs?.trim() ?? "", MAX_LOG_BYTES);
108113

109114
// Upload logs to Gist if present
110115
let gistUrl: string | undefined;
@@ -343,6 +348,19 @@ function truncateText(value: string, maxLength: number): string {
343348
return `${value.slice(0, maxLength)}\n\n[truncated]`;
344349
}
345350

351+
/**
352+
* Tail-truncate logs to at most `maxBytes` UTF-8 bytes. Diagnostic logs
353+
* are most useful at the END, so we drop the older lines when oversize.
354+
* Skips leading UTF-8 continuation bytes to land on a code-point boundary.
355+
*/
356+
function truncateLogTail(value: string, maxBytes: number): string {
357+
const bytes = new TextEncoder().encode(value);
358+
if (bytes.byteLength <= maxBytes) return value;
359+
let offset = bytes.byteLength - maxBytes;
360+
while (offset < bytes.byteLength && (bytes[offset] & 0xc0) === 0x80) offset++;
361+
return `[truncated: dropped ${offset} bytes of older logs]\n${new TextDecoder().decode(bytes.slice(offset))}`;
362+
}
363+
346364
function parseLabels(raw: string | undefined): string[] | undefined {
347365
const labels = raw
348366
?.split(",")

0 commit comments

Comments
 (0)