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
Empty file added .beads/.sync.lock
Empty file.
48 changes: 25 additions & 23 deletions .beads/issues.jsonl

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ junit-report.xml

# Local runtime/editor saves that should not be committed
src/.saves/

# Telemetry runtime output (prototype)
server/telemetry/events.ndjson
52 changes: 52 additions & 0 deletions server/telemetry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
Telemetry Receiver Prototype

Purpose:

This receiver is a development prototype for collecting telemetry events emitted by the Director and related runtime components. It is intended for local testing and experimentation only — not for production use. Use it to:

- Capture and inspect `director_decision` events emitted by the Director during playtests.
- Exercise telemetry payload shapes and validate downstream processing or analysis scripts.
- Provide a simple, disposable storage backend (newline-delimited JSON) for quick local debugging.

Do not rely on this receiver for production telemetry: it has no authentication, no retention/rotation, and minimal error handling.

Run locally:

- Node (>= 14) is required
- Start the receiver:

PORT=4005 node server/telemetry/receiver.js

It listens on `/` for HTTP POST JSON payloads.

Accepted events:

Only events with `type: "director_decision"` (or `event_type` or nested `event.type`) are accepted and persisted to `server/telemetry/events.ndjson`.

Expected payload shape (example):

{
"type": "director_decision",
"decision": "accept",
"reason": "low_risk",
"meta": { "user": "test" }
}

Example curl test:

curl -v -X POST \
-H "Content-Type: application/json" \
-d '{"type":"director_decision","decision":"accept","meta":{"user":"test"}}' \
http://localhost:4005/

Expected responses:
- 200 {"ok":true} for valid director_decision events
- 400 {"error":"Invalid or unsupported event type"} for invalid event types
- 400 {"error":"Invalid JSON"} for malformed JSON
- 404 for non-POST or other paths

Storage:
- Events are appended to `server/telemetry/events.ndjson` as newline-delimited JSON lines with a `received_at` timestamp.

Notes / next steps:
- This is intentionally minimal. For follow-up work consider adding SQLite persistence, simple schema validation, or basic authentication before using in shared environments.
87 changes: 87 additions & 0 deletions server/telemetry/receiver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env node
// Lightweight telemetry receiver prototype
// Accepts POST JSON events and appends director_decision events to events.ndjson

const http = require('http');
const fs = require('fs');
const path = require('path');

const PORT = process.env.PORT ? Number(process.env.PORT) : 4005;
const DATA_DIR = path.resolve(__dirname);
const OUTFILE = path.join(DATA_DIR, 'events.ndjson');

function isDirectorDecision(payload) {
if (!payload || typeof payload !== 'object') return false;
// Accept several possible fields that indicate event type
const t = payload.type || payload.event_type || (payload.event && payload.event.type) || null;
return t === 'director_decision';
}

const server = http.createServer((req, res) => {
if (req.method !== 'POST' || req.url !== '/') {
res.statusCode = 404;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Not found' }));
return;
}

let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
let payload;
try {
payload = JSON.parse(body || '{}');
} catch (err) {
res.statusCode = 400;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Invalid JSON' }));
return;
}

if (!isDirectorDecision(payload)) {
res.statusCode = 400;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Invalid or unsupported event type' }));
return;
}

const line = JSON.stringify({ received_at: new Date().toISOString(), payload });

fs.appendFile(OUTFILE, line + '\n', (err) => {
if (err) {
console.error('Failed to persist event', err);
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Failed to persist event' }));
return;
}

res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ ok: true }));
});
});

req.on('error', (err) => {
console.error('Request error', err);
res.statusCode = 400;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Bad request' }));
});
});

// Ensure data directory exists (it does) and touch outfile
try {
fs.mkdirSync(DATA_DIR, { recursive: true });
fs.openSync(OUTFILE, 'a');
} catch (err) {
console.error('Failed to prepare storage file:', err);
process.exit(1);
}

server.listen(PORT, () => {
console.log(`Telemetry receiver listening on http://localhost:${PORT}/`);
console.log(`Persisting director_decision events to ${OUTFILE}`);
});

module.exports = server; // for testing