Skip to content

Commit 557a82b

Browse files
authored
Merge pull request #182 from TheWizardsCode/feature/ge-hch.5.17-telemetry
WIP: Telemetry Implementation (bge-hch.5.17)
2 parents a9902e2 + ac4db1d commit 557a82b

File tree

14 files changed

+368
-85
lines changed

14 files changed

+368
-85
lines changed

.beads/issues.jsonl

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

.beads/sync_base.jsonl

Lines changed: 9 additions & 3 deletions
Large diffs are not rendered by default.

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** @type {import('jest').Config} */
22
module.exports = {
33
testEnvironment: 'jsdom',
4-
testMatch: ['**/tests/unit/**/*.test.[jt]s', '**/tests/validate-story/**/*.test.[jt]s', '**/tests/replay/**/*.spec.[jt]s'],
4+
testMatch: ['**/tests/unit/**/*.test.[jt]s', '**/tests/validate-story/**/*.test.[jt]s', '**/tests/replay/**/*.spec.[jt]s', '**/tests/integration/**/*.test.[jt]s'],
55
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
66
};

server/telemetry/README.md

Lines changed: 66 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,66 @@
1-
Telemetry Receiver Prototype
2-
3-
Purpose:
4-
5-
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:
6-
7-
- Capture and inspect `director_decision` events emitted by the Director during playtests.
8-
- Exercise telemetry payload shapes and validate downstream processing or analysis scripts.
9-
- Provide a simple, disposable storage backend (newline-delimited JSON) for quick local debugging.
10-
11-
Do not rely on this receiver for production telemetry: it has no authentication, no retention/rotation, and minimal error handling.
12-
13-
Run locally:
14-
15-
- Node (>= 14) is required
16-
- Start the receiver:
17-
18-
PORT=4005 node server/telemetry/receiver.js
19-
20-
It listens on `/` for HTTP POST JSON payloads.
21-
22-
Accepted events:
23-
24-
Only events with `type: "director_decision"` (or `event_type` or nested `event.type`) are accepted and persisted to `server/telemetry/events.ndjson`.
25-
26-
Expected payload shape (example):
27-
28-
{
29-
"type": "director_decision",
30-
"decision": "accept",
31-
"reason": "low_risk",
32-
"meta": { "user": "test" }
33-
}
34-
35-
Example curl test:
36-
37-
curl -v -X POST \
38-
-H "Content-Type: application/json" \
39-
-d '{"type":"director_decision","decision":"accept","meta":{"user":"test"}}' \
40-
http://localhost:4005/
41-
42-
Expected responses:
43-
- 200 {"ok":true} for valid director_decision events
44-
- 400 {"error":"Invalid or unsupported event type"} for invalid event types
45-
- 400 {"error":"Invalid JSON"} for malformed JSON
46-
- 404 for non-POST or other paths
47-
48-
Storage:
49-
- Events are appended to `server/telemetry/events.ndjson` as newline-delimited JSON lines with a `received_at` timestamp.
50-
51-
Notes / next steps:
52-
- This is intentionally minimal. For follow-up work consider adding SQLite persistence, simple schema validation, or basic authentication before using in shared environments.
1+
Telemetry receiver (dev prototype)
2+
3+
Purpose
4+
-------
5+
Lightweight development receiver that accepts POSTed JSON events and persists director decision telemetry for local analysis.
6+
7+
What it does
8+
------------
9+
- Accepts POST requests to `/` with a JSON body.
10+
- Validates that the event represents a `director_decision` (accepts payloads with `type: "director_decision"` or same under `event_type` or `event.type`).
11+
- Appends accepted events as NDJSON lines to `server/telemetry/events.ndjson` (dev ingestion store).
12+
13+
Run locally
14+
-----------
15+
```bash
16+
# starts the receiver on port 4005 by default
17+
node server/telemetry/receiver.js
18+
19+
# to choose a different port (useful in tests):
20+
PORT=0 node server/telemetry/receiver.js
21+
```
22+
23+
The process prints the listening URL to stdout when ready, e.g. `Telemetry receiver listening on http://localhost:4005/`.
24+
25+
API (single endpoint)
26+
---------------------
27+
- POST /
28+
- Content-Type: application/json
29+
- Body: arbitrary JSON representing an event
30+
- Success (200): when the payload identifies as a `director_decision` and was persisted
31+
- Client error (400): when payload is invalid JSON or not a supported event type
32+
- Server error (500): when writing to storage failed
33+
34+
Example payload (director_decision)
35+
----------------------------------
36+
```json
37+
{
38+
"type": "director_decision",
39+
"proposal_id": "p1",
40+
"decision": "approve",
41+
"riskScore": 0.12,
42+
"reason": "low_risk",
43+
"metrics": { "latencyMs": 120 }
44+
}
45+
```
46+
47+
Curl example
48+
------------
49+
```bash
50+
curl -X POST http://localhost:4005/ \
51+
-H 'Content-Type: application/json' \
52+
-d '{"type":"director_decision","proposal_id":"p1","decision":"approve","riskScore":0.12}'
53+
```
54+
55+
Inspecting persisted events
56+
---------------------------
57+
Events are appended to `server/telemetry/events.ndjson` as one JSON object per line. To inspect recent events:
58+
59+
```bash
60+
tail -n 50 server/telemetry/events.ndjson | jq .
61+
```
62+
63+
Development notes
64+
-----------------
65+
- This receiver is intentionally small and intended for dev/testing only. Production work (SQLite storage, schema validation, auth/token protection, log rotation) is tracked in `ge-apq.1` and should be implemented before using this in production.
66+
- The receiver uses `server/telemetry/backend-ndjson.js` as the storage backend; swap or extend backends as needed.

server/telemetry/backend-ndjson.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use strict'
2+
3+
const fs = require('fs')
4+
const path = require('path')
5+
6+
const LOG_DIR = path.join(__dirname)
7+
const LOG_FILE = path.join(LOG_DIR, 'events.ndjson')
8+
9+
function ensureDir() {
10+
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true })
11+
}
12+
13+
function emit(event) {
14+
try {
15+
ensureDir()
16+
// If event looks like a wrapped { received_at, payload } keep that shape
17+
if (event && event.received_at && event.payload) fs.appendFileSync(LOG_FILE, JSON.stringify(event) + '\n', 'utf8')
18+
else fs.appendFileSync(LOG_FILE, JSON.stringify({ received_at: new Date().toISOString(), payload: event }) + '\n', 'utf8')
19+
} catch (e) {
20+
console.error('ndjson backend write failed', e)
21+
}
22+
}
23+
24+
module.exports = { emit }

server/telemetry/receiver.js

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
const http = require('http');
66
const fs = require('fs');
77
const path = require('path');
8+
const ndjsonBackend = require('../telemetry/backend-ndjson')
89

910
const PORT = process.env.PORT ? Number(process.env.PORT) : 4005;
1011
const DATA_DIR = path.resolve(__dirname);
@@ -45,21 +46,19 @@ const server = http.createServer((req, res) => {
4546
return;
4647
}
4748

48-
const line = JSON.stringify({ received_at: new Date().toISOString(), payload });
49-
50-
fs.appendFile(OUTFILE, line + '\n', (err) => {
51-
if (err) {
52-
console.error('Failed to persist event', err);
53-
res.statusCode = 500;
54-
res.setHeader('Content-Type', 'application/json');
55-
res.end(JSON.stringify({ error: 'Failed to persist event' }));
56-
return;
57-
}
58-
49+
const event = { received_at: new Date().toISOString(), payload };
50+
// write via simple ndjson backend (appends to events.ndjson)
51+
try {
52+
ndjsonBackend.emit(event);
5953
res.statusCode = 200;
6054
res.setHeader('Content-Type', 'application/json');
6155
res.end(JSON.stringify({ ok: true }));
62-
});
56+
} catch (err) {
57+
console.error('Failed to persist event', err);
58+
res.statusCode = 500;
59+
res.setHeader('Content-Type', 'application/json');
60+
res.end(JSON.stringify({ error: 'Failed to persist event' }));
61+
}
6362
});
6463

6564
req.on('error', (err) => {
Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,31 @@
11
// Telemetry subscriber for runtime HookManager
22
// Emits console-based telemetry events; in prod this should hook into telemetry module
33

4-
module.exports = function createTelemetrySubscriber(telemetry = console) {
4+
const { defaultTelemetry } = require('../../telemetry/emitter');
5+
6+
// Map runtime hook names to telemetry event types
7+
const HOOK_EVENT_MAP = {
8+
pre_inject: 'generation',
9+
post_inject: 'presentation',
10+
pre_checkpoint: 'validation',
11+
post_checkpoint: 'outcome'
12+
};
13+
14+
module.exports = function createTelemetrySubscriber(telemetryBackend) {
15+
const telemetry = telemetryBackend || defaultTelemetry;
516
return {
617
name: 'runtime-telemetry-subscriber',
718
async pre_inject(payload) {
8-
try {
9-
telemetry.log('telemetry.event', { event: 'pre_inject', payload });
10-
} catch (err) {
11-
// swallow
12-
}
19+
try { telemetry.emit(HOOK_EVENT_MAP.pre_inject, payload); } catch (err) { }
1320
},
1421
async post_inject(payload) {
15-
try {
16-
telemetry.log('telemetry.event', { event: 'post_inject', payload });
17-
} catch (err) {}
22+
try { telemetry.emit(HOOK_EVENT_MAP.post_inject, payload); } catch (err) { }
1823
},
1924
async pre_checkpoint(payload) {
20-
try {
21-
telemetry.log('telemetry.event', { event: 'pre_checkpoint', payload });
22-
} catch (err) {}
25+
try { telemetry.emit(HOOK_EVENT_MAP.pre_checkpoint, payload); } catch (err) { }
2326
},
2427
async post_checkpoint(payload) {
25-
try {
26-
telemetry.log('telemetry.event', { event: 'post_checkpoint', payload });
27-
} catch (err) {}
28+
try { telemetry.emit(HOOK_EVENT_MAP.post_checkpoint, payload); } catch (err) { }
2829
}
2930
};
3031
};

src/telemetry/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Telemetry module
2+
3+
This lightweight telemetry module provides:
4+
5+
- `src/telemetry/emitter.js` — in-memory telemetry emitter with redact-on-ingest and a simple query API for tests and local analysis.
6+
- `src/telemetry/redact.js` — minimal PII redaction helpers.
7+
- `src/telemetry/backends/console.js` — default backend writing concise logs to console.
8+
9+
Usage (node):
10+
11+
```js
12+
const { defaultTelemetry } = require('./src/telemetry/emitter')
13+
const consoleBackend = require('./src/telemetry/backends/console')
14+
defaultTelemetry.addBackend(consoleBackend)
15+
defaultTelemetry.emit('story_start', { sessionId: 's1', userEmail: '[email protected]' })
16+
```
17+
18+
Notes
19+
- Redaction is intentionally conservative; extend `redact.js` for stricter rules.
20+
- Buffer size defaults to 1000 events; override via `new Telemetry({bufferSize})` if needed.

src/telemetry/backends/console.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use strict'
2+
3+
function emit(event) {
4+
// keep a concise log format
5+
try {
6+
console.log('[TELEMETRY]', event.type, event.timestamp, JSON.stringify(event.payload))
7+
} catch (e) {
8+
console.log('[TELEMETRY]', event.type, event.timestamp)
9+
}
10+
}
11+
12+
module.exports = { emit }

src/telemetry/emitter.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Telemetry emitter: emits events to available backends and provides an in-memory/queryable store for tests.
2+
'use strict'
3+
4+
const { redact } = require('./redact')
5+
6+
const DEFAULT_BUFFER_SIZE = 1000
7+
8+
class Telemetry {
9+
constructor(opts = {}) {
10+
this.bufferSize = opts.bufferSize || DEFAULT_BUFFER_SIZE
11+
this.events = [] // circular buffer
12+
this.backends = []
13+
}
14+
15+
addBackend(backend) {
16+
if (backend && typeof backend.emit === 'function') this.backends.push(backend)
17+
}
18+
19+
emit(type, payload = {}) {
20+
const ts = new Date().toISOString()
21+
const redacted = redact(payload)
22+
const event = { type, timestamp: ts, payload: redacted }
23+
// validate against schema if available
24+
try {
25+
const { validate } = require('./schema')
26+
const res = validate(type, redacted)
27+
if (!res.valid) {
28+
// emit a validation event instead of storing the invalid payload
29+
const v = { type: 'validation', timestamp: ts, payload: { valid: false, errors: res.errors, originalType: type } }
30+
this._push(v)
31+
for (const b of this.backends) { try { b.emit(v) } catch (e) {} }
32+
return
33+
}
34+
} catch (e) {
35+
// ignore schema failures
36+
}
37+
38+
this._push(event)
39+
for (const b of this.backends) {
40+
try { b.emit(event) } catch (e) { console.error('telemetry backend emit failed', e) }
41+
}
42+
}
43+
44+
_push(event) {
45+
this.events.push(event)
46+
if (this.events.length > this.bufferSize) this.events.shift()
47+
}
48+
49+
query(filterFn) {
50+
if (!filterFn) return this.events.slice()
51+
return this.events.filter(filterFn)
52+
}
53+
54+
clear() { this.events = [] }
55+
}
56+
57+
// Singleton for browser/demo usage
58+
const defaultTelemetry = new Telemetry()
59+
60+
module.exports = { Telemetry, defaultTelemetry }

0 commit comments

Comments
 (0)