Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion PROOF.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ to the trailing window; (4) leaked snapshot timers on unmount → removed. All p
## 5. M2 — drill-down (click a comet → its real span, verified against `/traces/:id`)

Clicking a comet resolves it to the correct span and shows that span's **real attributes**. The pick is
pure CPU math (`app/src/gpu/motion.ts`) that mirrors the shader's closed-form motion (`shaders/river.wgsl`)
pure CPU math (`app/src/gpu/motion.ts`) that mirrors the shader's closed-form motion (`shaders/river-sim.wgsl`; M4 moved the motion authority here from `river.wgsl`, which is now the draw shader)
and inverts a screen click back to the nearest comet head; the server now retains each span's raw
attributes and serves them at `GET /traces/:id` (the SSE stream stays lean — attributes are fetched on
demand, not streamed).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,4 @@ develop the renderer.
- ffmpeg isn't required; without it you get `.webm` (plays everywhere). With it, `record.mjs` also emits `.mp4` + `.gif`.

## Requirements
node ≥ 20 · python 3 (for `npm run serve`) · a WebGPU-capable Chrome/Edge. See [`CLAUDE.md`](./CLAUDE.md).
node ≥ 20 · python 3 (for `npm run serve:spike`) · a WebGPU-capable Chrome/Edge. See [`CLAUDE.md`](./CLAUDE.md).
2 changes: 1 addition & 1 deletion ingest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const events = sorted.map((r) => {
cacheHit: r.cacheHit === true,
fallbackUsed: r.fallbackUsed === true,
guardrail: r.guardrailStatus || null, // 'pass' | 'flag' | 'block'
pii: piiCategories.length > 0,
pii: piiCategories.length > 0 || r.guardrailStatus === 'block', // match server mappers (otlp.ts/poller.ts)
piiCategories,
};
});
Expand Down
12 changes: 10 additions & 2 deletions server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,18 @@ import { startPoller } from './poller';
const PORT = Number(process.env.PORT ?? 4319);
const hub = new Hub(Number(process.env.BUFFER ?? 2000));

const MAX_BODY = 8 * 1024 * 1024; // 8 MB cap on the OTLP POST body — the only network-facing write path
class BodyTooLarge extends Error {}

function readBody(req: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
let b = '';
req.on('data', (c) => (b += c));
let size = 0;
req.on('data', (c) => {
size += c.length;
if (size > MAX_BODY) { req.destroy(); reject(new BodyTooLarge('request body too large')); return; }
b += c;
});
req.on('end', () => resolve(b));
req.on('error', reject);
});
Expand All @@ -47,7 +55,7 @@ const server = createServer(async (req, res) => {
res.end('{}');
if (events.length) console.log(`[otlp] +${events.length} span(s) buffer=${hub.size} clients=${hub.clientCount}`);
} catch (e) {
res.writeHead(400, { 'content-type': 'application/json' });
res.writeHead(e instanceof BodyTooLarge ? 413 : 400, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: String(e) }));
}
return;
Expand Down
6 changes: 5 additions & 1 deletion shared/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ export type Guardrail = 'pass' | 'flag' | 'block' | null;
export interface TraceEvent {
/** stable id (span id) — optional until the OTLP/poller path fills it (M1/M2 drill-down). */
id?: string;
/** ms from the first event in the trace. */
/**
* File/replay path: ms from the first event in the trace (`ingest.mjs` relativizes to t0).
* Live server path (OTLP/poller): absolute epoch ms — there is no single trace t0 in a
* streaming ring buffer; live consumers position comets by arrival time, not `t`.
*/
t: number;
model: string | null;
provider: string | null;
Expand Down
Loading