From a68bab5cff11bb1cde770b47b5d30beebcfe20e5 Mon Sep 17 00:00:00 2001 From: theMackabu Date: Sat, 25 Apr 2026 17:04:42 -0700 Subject: [PATCH] split/clean up code in js.reports --- docs/reports/src/env.ts | 4 + docs/reports/src/helpers.ts | 63 +++++++++ docs/reports/src/index.ts | 248 ------------------------------------ docs/reports/src/reports.ts | 114 +++++++++++++++++ docs/reports/src/routes.ts | 74 +++++++++++ docs/reports/wrangler.toml | 2 +- 6 files changed, 256 insertions(+), 249 deletions(-) create mode 100644 docs/reports/src/env.ts create mode 100644 docs/reports/src/helpers.ts delete mode 100644 docs/reports/src/index.ts create mode 100644 docs/reports/src/reports.ts create mode 100644 docs/reports/src/routes.ts diff --git a/docs/reports/src/env.ts b/docs/reports/src/env.ts new file mode 100644 index 0000000..0a9e7de --- /dev/null +++ b/docs/reports/src/env.ts @@ -0,0 +1,4 @@ +export type Bindings = { + ASSETS: Fetcher; + DB: D1Database; +}; diff --git a/docs/reports/src/helpers.ts b/docs/reports/src/helpers.ts new file mode 100644 index 0000000..700d522 --- /dev/null +++ b/docs/reports/src/helpers.ts @@ -0,0 +1,63 @@ +import type { Bindings } from './env'; + +let antLogoDataUrl: Promise | null = null; +let ogFontBytes: Promise | null = null; + +export async function sha256(input: string): Promise { + const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input)); + return [...new Uint8Array(digest)].map(byte => byte.toString(16).padStart(2, '0')).join(''); +} + +export function publicUrl(requestUrl: string, id: string): string { + const url = new URL(requestUrl); + return `${url.origin}/${id}`; +} + +export function publicOgImageUrl(requestUrl: string, id: string): string { + const url = new URL(requestUrl); + return `${url.origin}/og/${id}.png`; +} + +export function ogImageId(value: string): string { + return value.endsWith('.png') ? value.slice(0, -4) : value; +} + +function bytesToBase64(bytes: Uint8Array): string { + let binary = ''; + const chunkSize = 0x8000; + for (let i = 0; i < bytes.length; i += chunkSize) { + binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize)); + } + return btoa(binary); +} + +async function loadAntLogoDataUrl(env: Bindings, requestUrl: string): Promise { + const response = await env.ASSETS.fetch(new Request(new URL('/assets/ant.png', requestUrl))); + if (!response.ok) return ''; + const bytes = new Uint8Array(await response.arrayBuffer()); + return `data:image/png;base64,${bytesToBase64(bytes)}`; +} + +async function loadAssetBytes( + env: Bindings, + requestUrl: string, + path: string, +): Promise { + const response = await env.ASSETS.fetch(new Request(new URL(path, requestUrl))); + if (!response.ok) return new Uint8Array(); + return new Uint8Array(await response.arrayBuffer()); +} + +export function getAntLogoDataUrl(env: Bindings, requestUrl: string): Promise { + antLogoDataUrl ??= loadAntLogoDataUrl(env, requestUrl); + return antLogoDataUrl; +} + +export function getOgFontBytes(env: Bindings, requestUrl: string): Promise { + ogFontBytes ??= Promise.all([ + loadAssetBytes(env, requestUrl, '/assets/Arial.ttf'), + loadAssetBytes(env, requestUrl, '/assets/Arial-Bold.ttf'), + loadAssetBytes(env, requestUrl, '/assets/BerkeleyMono-Regular.woff2'), + ]); + return ogFontBytes; +} diff --git a/docs/reports/src/index.ts b/docs/reports/src/index.ts deleted file mode 100644 index 94008e1..0000000 --- a/docs/reports/src/index.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { Hono } from 'hono'; -import { renderOgPng } from './og'; -import { asc, eq } from 'drizzle-orm'; -import { drizzle } from 'drizzle-orm/d1'; -import { renderBlank, renderReport } from './view'; -import { generate as generateShortUuid } from 'short-uuid'; - -import { - CrashReportSchema, - frames as frameTable, - reportFrames, - reports, - type CrashReport, -} from './schema'; - -type Bindings = { ASSETS: Fetcher; DB: D1Database }; -const app = new Hono<{ Bindings: Bindings }>(); - -let antLogoDataUrl: Promise | null = null; -let ogFontBytes: Promise | null = null; - -app.get('/', c => c.html(renderBlank(), 404)); - -async function reportTrace(report: CrashReport): Promise { - const traceInput = JSON.stringify({ - runtime: report.runtime, - version: report.version, - target: report.target, - os: report.os, - arch: report.arch, - code: report.code, - reason: report.reason, - addr: report.addr, - frames: report.frames, - }); - - return sha256(traceInput); -} - -async function sha256(input: string): Promise { - const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input)); - return [...new Uint8Array(digest)].map(byte => byte.toString(16).padStart(2, '0')).join(''); -} - -function publicUrl(requestUrl: string, id: string): string { - const url = new URL(requestUrl); - return `${url.origin}/${id}`; -} - -function publicOgImageUrl(requestUrl: string, id: string): string { - const url = new URL(requestUrl); - return `${url.origin}/og/${id}.png`; -} - -function bytesToBase64(bytes: Uint8Array): string { - let binary = ''; - const chunkSize = 0x8000; - for (let i = 0; i < bytes.length; i += chunkSize) { - binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize)); - } - return btoa(binary); -} - -async function loadAntLogoDataUrl(env: Bindings, requestUrl: string): Promise { - const response = await env.ASSETS.fetch(new Request(new URL('/assets/ant.png', requestUrl))); - if (!response.ok) return ''; - const bytes = new Uint8Array(await response.arrayBuffer()); - return `data:image/png;base64,${bytesToBase64(bytes)}`; -} - -async function loadAssetBytes( - env: Bindings, - requestUrl: string, - path: string, -): Promise { - const response = await env.ASSETS.fetch(new Request(new URL(path, requestUrl))); - if (!response.ok) return new Uint8Array(); - return new Uint8Array(await response.arrayBuffer()); -} - -function getAntLogoDataUrl(env: Bindings, requestUrl: string): Promise { - antLogoDataUrl ??= loadAntLogoDataUrl(env, requestUrl); - return antLogoDataUrl; -} - -function ogImageId(value: string): string { - return value.endsWith('.png') ? value.slice(0, -4) : value; -} - -function getOgFontBytes(env: Bindings, requestUrl: string): Promise { - ogFontBytes ??= Promise.all([ - loadAssetBytes(env, requestUrl, '/assets/Arial.ttf'), - loadAssetBytes(env, requestUrl, '/assets/Arial-Bold.ttf'), - loadAssetBytes(env, requestUrl, '/assets/BerkeleyMono-Regular.woff2'), - ]); - return ogFontBytes; -} - -const reportFromRows = ( - row: typeof reports.$inferSelect, - frameRows: { frame: string }[], -): CrashReport => ({ - schema: 1, - runtime: 'ant', - version: row.version, - target: row.target, - os: row.os, - arch: row.arch, - kind: row.kind, - code: row.code, - reason: row.reason, - addr: row.faultAddress, - elapsedMs: row.elapsedMs, - peakRss: row.peakRss, - frames: frameRows.map(row => row.frame), -}); - -async function insertReportFrames( - db: ReturnType, - reportId: string, - frames: string[], -): Promise { - if (!frames.length) return; - const frameHashes = await Promise.all(frames.map(frame => sha256(frame))); - const uniqueFrames = new Map(); - - frames.forEach((frame, index) => { - uniqueFrames.set(frameHashes[index], frame); - }); - - await db - .insert(frameTable) - .values([...uniqueFrames].map(([hash, frame]) => ({ hash, frame }))) - .onConflictDoNothing(); - - await db.insert(reportFrames).values( - frameHashes.map((frameHash, index) => ({ - frameHash, - reportId, - frameIndex: index, - })), - ); -} - -async function getReportFrames( - db: ReturnType, - reportId: string, -): Promise<{ frame: string }[]> { - return db - .select({ - frame: frameTable.frame, - }) - .from(reportFrames) - .innerJoin(frameTable, eq(reportFrames.frameHash, frameTable.hash)) - .where(eq(reportFrames.reportId, reportId)) - .orderBy(asc(reportFrames.frameIndex)); -} - -async function getReport(db: ReturnType, id: string): Promise { - const [row] = await db.select().from(reports).where(eq(reports.id, id)).limit(1); - - if (!row) return null; - const frames = await getReportFrames(db, row.id); - return reportFromRows(row, frames); -} - -app.get('/og/:image', async c => { - const db = drizzle(c.env.DB); - const id = ogImageId(c.req.param('image')); - - const cache = caches.default; - const cacheKey = new Request(publicOgImageUrl(c.req.url, id), { method: 'GET' }); - - const cached = await cache.match(cacheKey); - if (cached) return cached; - - const report = await getReport(db, id); - if (!report) return c.notFound(); - - const png = await renderOgPng( - report, - await getAntLogoDataUrl(c.env, c.req.url), - await getOgFontBytes(c.env, c.req.url), - ); - - const response = c.body(png, 200, { - 'Content-Type': 'image/png', - 'Cache-Control': 'public, max-age=2592000, immutable', - }); - - c.executionCtx.waitUntil(cache.put(cacheKey, response.clone())); - return response; -}); - -app.get('/:id', async c => { - const db = drizzle(c.env.DB); - const id = c.req.param('id'); - const report = await getReport(db, id); - - if (!report) return c.html(renderBlank(), 404); - return c.html(renderReport(report, publicUrl(c.req.url, id), publicOgImageUrl(c.req.url, id))); -}); - -app.post('/report', async c => { - let rawReport: unknown; - try { - rawReport = await c.req.json(); - } catch { - return c.json({ error: 'invalid payload' }, 400); - } - - const parsedReport = CrashReportSchema.safeParse(rawReport); - if (!parsedReport.success) return c.json({ error: 'invalid report' }, 400); - - const report = parsedReport.data; - const trace = await reportTrace(report); - - const db = drizzle(c.env.DB); - const id = generateShortUuid(); - - const now = new Date(); - const expires = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); - - await db.insert(reports).values({ - id, - trace, - hitCount: 1, - runtime: report.runtime, - version: report.version, - kind: report.kind, - reason: report.reason, - code: report.code, - target: report.target, - os: report.os, - arch: report.arch, - faultAddress: report.addr, - elapsedMs: report.elapsedMs, - peakRss: report.peakRss, - firstSeenAt: now.toISOString(), - lastSeenAt: now.toISOString(), - expiresAt: expires.toISOString(), - }); - - await insertReportFrames(db, id, report.frames); - return c.text(publicUrl(c.req.url, id), 200); -}); - -export default app; diff --git a/docs/reports/src/reports.ts b/docs/reports/src/reports.ts new file mode 100644 index 0000000..830d04f --- /dev/null +++ b/docs/reports/src/reports.ts @@ -0,0 +1,114 @@ +import { sha256 } from './helpers'; +import { asc, eq } from 'drizzle-orm'; +import { drizzle } from 'drizzle-orm/d1'; +import { generate as generateShortUuid } from 'short-uuid'; +import { frames as frameTable, reportFrames, reports, type CrashReport } from './schema'; + +type ReportDb = ReturnType; + +async function reportTrace(report: CrashReport): Promise { + const traceInput = JSON.stringify({ + runtime: report.runtime, + version: report.version, + target: report.target, + os: report.os, + arch: report.arch, + code: report.code, + reason: report.reason, + addr: report.addr, + frames: report.frames, + }); + + return sha256(traceInput); +} + +const reportFromRows = ( + row: typeof reports.$inferSelect, + frameRows: { frame: string }[], +): CrashReport => ({ + schema: 1, + runtime: 'ant', + version: row.version, + target: row.target, + os: row.os, + arch: row.arch, + kind: row.kind, + code: row.code, + reason: row.reason, + addr: row.faultAddress, + elapsedMs: row.elapsedMs, + peakRss: row.peakRss, + frames: frameRows.map(row => row.frame), +}); + +async function insertReportFrames(db: ReportDb, reportId: string, frames: string[]): Promise { + if (!frames.length) return; + const frameHashes = await Promise.all(frames.map(frame => sha256(frame))); + const uniqueFrames = new Map(); + + frames.forEach((frame, index) => { + uniqueFrames.set(frameHashes[index], frame); + }); + + await db + .insert(frameTable) + .values([...uniqueFrames].map(([hash, frame]) => ({ hash, frame }))) + .onConflictDoNothing(); + + await db.insert(reportFrames).values( + frameHashes.map((frameHash, index) => ({ + frameHash, + reportId, + frameIndex: index, + })), + ); +} + +async function getReportFrames(db: ReportDb, reportId: string): Promise<{ frame: string }[]> { + return db + .select({ + frame: frameTable.frame, + }) + .from(reportFrames) + .innerJoin(frameTable, eq(reportFrames.frameHash, frameTable.hash)) + .where(eq(reportFrames.reportId, reportId)) + .orderBy(asc(reportFrames.frameIndex)); +} + +export async function getReport(db: ReportDb, id: string): Promise { + const [row] = await db.select().from(reports).where(eq(reports.id, id)).limit(1); + + if (!row) return null; + const frames = await getReportFrames(db, row.id); + return reportFromRows(row, frames); +} + +export async function createReport(db: ReportDb, report: CrashReport): Promise { + const id = generateShortUuid(); + const trace = await reportTrace(report); + const now = new Date(); + const expires = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); + + await db.insert(reports).values({ + id, + trace, + hitCount: 1, + runtime: report.runtime, + version: report.version, + kind: report.kind, + reason: report.reason, + code: report.code, + target: report.target, + os: report.os, + arch: report.arch, + faultAddress: report.addr, + elapsedMs: report.elapsedMs, + peakRss: report.peakRss, + firstSeenAt: now.toISOString(), + lastSeenAt: now.toISOString(), + expiresAt: expires.toISOString(), + }); + + await insertReportFrames(db, id, report.frames); + return id; +} diff --git a/docs/reports/src/routes.ts b/docs/reports/src/routes.ts new file mode 100644 index 0000000..a37a4e1 --- /dev/null +++ b/docs/reports/src/routes.ts @@ -0,0 +1,74 @@ +import { Hono } from 'hono'; +import { renderOgPng } from './og'; +import type { Bindings } from './env'; +import { drizzle } from 'drizzle-orm/d1'; +import { CrashReportSchema } from './schema'; +import { renderBlank, renderReport } from './view'; +import { createReport, getReport } from './reports'; + +import { + getAntLogoDataUrl, + getOgFontBytes, + ogImageId, + publicOgImageUrl, + publicUrl, +} from './helpers'; + +const app = new Hono<{ Bindings: Bindings }>(); + +app.get('/', c => c.html(renderBlank(), 404)); + +app.get('/og/:image', async c => { + const db = drizzle(c.env.DB); + const id = ogImageId(c.req.param('image')); + + const cache = caches.default; + const cacheKey = new Request(publicOgImageUrl(c.req.url, id), { method: 'GET' }); + + const cached = await cache.match(cacheKey); + if (cached) return cached; + + const report = await getReport(db, id); + if (!report) return c.notFound(); + + const png = await renderOgPng( + report, + await getAntLogoDataUrl(c.env, c.req.url), + await getOgFontBytes(c.env, c.req.url), + ); + + const response = c.body(png, 200, { + 'Content-Type': 'image/png', + 'Cache-Control': 'public, max-age=2592000, immutable', + }); + + c.executionCtx.waitUntil(cache.put(cacheKey, response.clone())); + return response; +}); + +app.get('/:id', async c => { + const db = drizzle(c.env.DB); + const id = c.req.param('id'); + const report = await getReport(db, id); + + if (!report) return c.html(renderBlank(), 404); + return c.html(renderReport(report, publicUrl(c.req.url, id), publicOgImageUrl(c.req.url, id))); +}); + +app.post('/report', async c => { + let rawReport: unknown; + try { + rawReport = await c.req.json(); + } catch { + return c.json({ error: 'invalid payload' }, 400); + } + + const parsedReport = CrashReportSchema.safeParse(rawReport); + if (!parsedReport.success) return c.json({ error: 'invalid report' }, 400); + + const db = drizzle(c.env.DB); + const id = await createReport(db, parsedReport.data); + return c.text(publicUrl(c.req.url, id), 200); +}); + +export default app; diff --git a/docs/reports/wrangler.toml b/docs/reports/wrangler.toml index d00dad9..56198ec 100644 --- a/docs/reports/wrangler.toml +++ b/docs/reports/wrangler.toml @@ -1,5 +1,5 @@ name = "js-report" -main = "src/index.ts" +main = "src/routes.ts" workers_dev = false compatibility_date = "2026-04-25" -- 2.51.2