Error Feedback Service + friendly error handling across all pages
- Error Feedback Service: POST /api/feedback saves to JSONL, GET with admin token - ErrorFeedback component: warm amber tones, friendly messages, 'Report Issue' button - Replaced ALL red-themed error displays across 15+ pages with ErrorFeedback - Kind classification: network, auth, location, upgrade, default messages - QA test suite v2: 8-layer testing (system, API, render, scenario, edge, mobile, perf, security) - Browser-based visual QA with Playwright (Layer 9)
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { mkdir, writeFile, readFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const DATA_DIR = "/app/data";
|
||||
const FEEDBACK_FILE = join(DATA_DIR, "feedback.jsonl");
|
||||
|
||||
interface FeedbackEntry {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
url: string;
|
||||
error: string;
|
||||
message: string;
|
||||
userAgent: string;
|
||||
}
|
||||
|
||||
function generateId(): string {
|
||||
const ts = Date.now().toString(36);
|
||||
const rand = Math.random().toString(36).substring(2, 8);
|
||||
return `fb-${ts}-${rand}`;
|
||||
}
|
||||
|
||||
function sanitizeUA(ua: string): string {
|
||||
// Strip personal info from UA string
|
||||
return ua.replace(/\(.*?\)/g, "(redacted)").substring(0, 200);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { url, error, message, userAgent } = body;
|
||||
|
||||
if (!url || !error) {
|
||||
return NextResponse.json(
|
||||
{ error: "url and error are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const entry: FeedbackEntry = {
|
||||
id: generateId(),
|
||||
timestamp: new Date().toISOString(),
|
||||
url: url.substring(0, 500),
|
||||
error: error.substring(0, 1000),
|
||||
message: (message || "").substring(0, 2000),
|
||||
userAgent: sanitizeUA(userAgent || ""),
|
||||
};
|
||||
|
||||
// Ensure data dir exists
|
||||
await mkdir(DATA_DIR, { recursive: true });
|
||||
|
||||
// Append to JSONL file
|
||||
await writeFile(FEEDBACK_FILE, JSON.stringify(entry) + "\n", {
|
||||
flag: "a",
|
||||
});
|
||||
|
||||
return NextResponse.json({ id: entry.id });
|
||||
} catch (err) {
|
||||
// Silent failure — don't throw errors from error-reporting endpoint
|
||||
console.error("Feedback save error:", err);
|
||||
return NextResponse.json({ id: null }, { status: 200 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
// Admin-only endpoint — return paginated feedback
|
||||
const authHeader = request.headers.get("authorization") || "";
|
||||
const adminToken = process.env.FEEDBACK_ADMIN_TOKEN || "flh-feedback-admin";
|
||||
|
||||
// Simple token auth
|
||||
if (authHeader !== `Bearer ${adminToken}`) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const limit = Math.min(parseInt(searchParams.get("limit") || "50"), 200);
|
||||
const offset = parseInt(searchParams.get("offset") || "0");
|
||||
|
||||
try {
|
||||
if (!existsSync(FEEDBACK_FILE)) {
|
||||
return NextResponse.json({ entries: [], total: 0 });
|
||||
}
|
||||
|
||||
const raw = await readFile(FEEDBACK_FILE, "utf-8");
|
||||
const lines = raw.trim().split("\n").filter(Boolean);
|
||||
const entries = lines
|
||||
.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line) as FeedbackEntry;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.reverse(); // newest first
|
||||
|
||||
const total = entries.length;
|
||||
const page = entries.slice(offset, offset + limit);
|
||||
|
||||
return NextResponse.json({ entries: page, total });
|
||||
} catch (err) {
|
||||
console.error("Feedback read error:", err);
|
||||
return NextResponse.json({ entries: [], total: 0 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user