feat(site-builder): add pure issue-report payload builder
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { buildReportPayload, MAX_PAYLOAD_BYTES } from './report-payload';
|
||||
|
||||
const base = {
|
||||
category: 'bug' as const,
|
||||
description: 'The HTML block colours do nothing',
|
||||
includeCanvas: true,
|
||||
siteId: 42,
|
||||
siteDomain: 'example.com',
|
||||
pageId: 'home',
|
||||
pageSlug: 'index',
|
||||
editorVersion: 'abc1234-2026-08-08',
|
||||
userAgent: 'Mozilla/5.0 test',
|
||||
viewport: '1920x1080',
|
||||
deviceMode: 'desktop',
|
||||
selectedType: 'HTML',
|
||||
consoleErrors: [{ ts: 1, message: 'oops' }],
|
||||
canvasState: '{"ROOT":{}}',
|
||||
};
|
||||
|
||||
describe('buildReportPayload', () => {
|
||||
test('carries every context field through', () => {
|
||||
const p = buildReportPayload(base);
|
||||
expect(p.category).toBe('bug');
|
||||
expect(p.description).toBe('The HTML block colours do nothing');
|
||||
expect(p.site_id).toBe(42);
|
||||
expect(p.site_domain).toBe('example.com');
|
||||
expect(p.page_slug).toBe('index');
|
||||
expect(p.editor_version).toBe('abc1234-2026-08-08');
|
||||
expect(p.device_mode).toBe('desktop');
|
||||
expect(p.selected_type).toBe('HTML');
|
||||
expect(p.console_errors).toEqual([{ ts: 1, message: 'oops' }]);
|
||||
expect(p.canvas_state).toBe('{"ROOT":{}}');
|
||||
expect(p.canvas_state_omitted).toBeUndefined();
|
||||
});
|
||||
|
||||
test('includeCanvas=false drops the canvas and records why', () => {
|
||||
const p = buildReportPayload({ ...base, includeCanvas: false });
|
||||
expect(p.canvas_state).toBeNull();
|
||||
expect(p.canvas_state_omitted).toBe('opt-out');
|
||||
expect(p.console_errors).toHaveLength(1);
|
||||
expect(p.site_domain).toBe('example.com');
|
||||
});
|
||||
|
||||
test('an oversized canvas is dropped rather than truncated', () => {
|
||||
const huge = 'x'.repeat(MAX_PAYLOAD_BYTES + 1000);
|
||||
const p = buildReportPayload({ ...base, canvasState: huge });
|
||||
expect(p.canvas_state).toBeNull();
|
||||
expect(p.canvas_state_omitted).toBe('size');
|
||||
});
|
||||
|
||||
test('the resulting payload always fits under the cap', () => {
|
||||
const huge = 'x'.repeat(MAX_PAYLOAD_BYTES * 2);
|
||||
const p = buildReportPayload({ ...base, canvasState: huge });
|
||||
expect(new Blob([JSON.stringify(p)]).size).toBeLessThanOrEqual(MAX_PAYLOAD_BYTES);
|
||||
});
|
||||
|
||||
test('description is trimmed', () => {
|
||||
expect(buildReportPayload({ ...base, description: ' spaced ' }).description).toBe('spaced');
|
||||
});
|
||||
|
||||
test('a null canvasState is reported as opt-out-free but still null', () => {
|
||||
const p = buildReportPayload({ ...base, canvasState: null });
|
||||
expect(p.canvas_state).toBeNull();
|
||||
expect(p.canvas_state_omitted).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { ConsoleErrorEntry } from './console-buffer';
|
||||
|
||||
/**
|
||||
* Assembles the JSON body for an in-builder issue report.
|
||||
*
|
||||
* Pure: every environment value (user agent, viewport, serialized canvas)
|
||||
* is passed IN rather than read from globals, so the whole thing is testable
|
||||
* without a DOM and the caller decides what it is willing to send.
|
||||
*
|
||||
* The canvas state is the only field that can be large. When it would push
|
||||
* the body over the cap it is DROPPED WHOLE and flagged -- a truncated craft
|
||||
* state is not merely useless, it is misleading (it looks like a valid tree
|
||||
* that lost nodes).
|
||||
*/
|
||||
|
||||
export const MAX_PAYLOAD_BYTES = 512 * 1024;
|
||||
|
||||
export type ReportCategory = 'bug' | 'confusing' | 'feature';
|
||||
|
||||
export interface BuildReportPayloadInput {
|
||||
category: ReportCategory;
|
||||
description: string;
|
||||
includeCanvas: boolean;
|
||||
siteId: number | null;
|
||||
siteDomain: string;
|
||||
pageId: string;
|
||||
pageSlug: string;
|
||||
editorVersion: string;
|
||||
userAgent: string;
|
||||
viewport: string;
|
||||
deviceMode: string;
|
||||
selectedType: string | null;
|
||||
consoleErrors: ConsoleErrorEntry[];
|
||||
canvasState: string | null;
|
||||
}
|
||||
|
||||
export interface ReportPayload {
|
||||
category: ReportCategory;
|
||||
description: string;
|
||||
site_id: number | null;
|
||||
site_domain: string;
|
||||
page_id: string;
|
||||
page_slug: string;
|
||||
editor_version: string;
|
||||
user_agent: string;
|
||||
viewport: string;
|
||||
device_mode: string;
|
||||
selected_type: string | null;
|
||||
console_errors: ConsoleErrorEntry[];
|
||||
canvas_state: string | null;
|
||||
/** Present only when the canvas state was dropped. */
|
||||
canvas_state_omitted?: 'size' | 'opt-out';
|
||||
}
|
||||
|
||||
function byteLength(value: string): number {
|
||||
if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(value).length;
|
||||
return value.length;
|
||||
}
|
||||
|
||||
export function buildReportPayload(input: BuildReportPayloadInput): ReportPayload {
|
||||
const payload: ReportPayload = {
|
||||
category: input.category,
|
||||
description: input.description.trim(),
|
||||
site_id: input.siteId,
|
||||
site_domain: input.siteDomain,
|
||||
page_id: input.pageId,
|
||||
page_slug: input.pageSlug,
|
||||
editor_version: input.editorVersion,
|
||||
user_agent: input.userAgent,
|
||||
viewport: input.viewport,
|
||||
device_mode: input.deviceMode,
|
||||
selected_type: input.selectedType,
|
||||
console_errors: input.consoleErrors,
|
||||
canvas_state: null,
|
||||
};
|
||||
|
||||
if (!input.includeCanvas) {
|
||||
payload.canvas_state_omitted = 'opt-out';
|
||||
return payload;
|
||||
}
|
||||
|
||||
if (!input.canvasState) return payload;
|
||||
|
||||
payload.canvas_state = input.canvasState;
|
||||
if (byteLength(JSON.stringify(payload)) > MAX_PAYLOAD_BYTES) {
|
||||
payload.canvas_state = null;
|
||||
payload.canvas_state_omitted = 'size';
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
Reference in New Issue
Block a user