Skip to content

Commit 2619df4

Browse files
committed
Implement basic telemetry module: emitter, redact, console backend, README
1 parent a9902e2 commit 2619df4

5 files changed

Lines changed: 114 additions & 1 deletion

File tree

.beads/issues.jsonl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@
202202
{"id":"ge-mud","title":"Make Ooda loop ouput fit the available screen width","status":"tombstone","priority":1,"issue_type":"task","assignee":"patch","created_at":"2026-01-16T22:21:19.755930775-08:00","created_by":"rgardler","updated_at":"2026-01-16T22:21:54.341660423-08:00","deleted_at":"2026-01-16T22:21:54.341660423-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
203203
{"id":"ge-ngf","title":"CI: Playwright E2E","description":"Add GitHub Actions workflow to run Playwright E2E tests.\\n\\nAcceptance criteria:\\n- Workflow file .github/workflows/playwright.yml runs on PRs and main.\\n- Workflow runs: npm ci, npx playwright install, npm test (demo e2e).\\n- On PR a job runs tests and reports status to PR.","status":"closed","priority":1,"issue_type":"task","assignee":"rgardler","created_at":"2026-01-06T23:08:53.428619454-08:00","created_by":"rgardler","updated_at":"2026-01-07T02:20:17.470750673-08:00","closed_at":"2026-01-07T02:20:17.470750673-08:00","close_reason":"Closed"}
204204
{"id":"ge-nzz","title":"Make root README InkJS-only","description":"Remove Unity references from the root README.md and focus it on InkJS/web demo usage and tests.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-06T15:12:16.621516991-08:00","created_by":"rgardler","updated_at":"2026-01-06T15:13:37.065401561-08:00","closed_at":"2026-01-06T15:13:37.065401561-08:00","close_reason":"Done"}
205-
{"id":"ge-okh","title":"Add npm script + docs to run embedding integration test","description":"Add an npm script to run the real-model embedding integration test (EMBED_NODE=1). Include a short note in web/demo/README.md describing the script and how to run it locally.\\n\\nAcceptance criteria:\\n- package.json has script that runs: \\n- web/demo/README.md contains a 1-2 line note explaining the script and env flags\\n- Tests: running the script locally succeeds (developer responsibility)\\n\\nFiles to be changed: , ","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-17T20:47:17.395948499-08:00","created_by":"rgardler","updated_at":"2026-01-17T20:47:17.395948499-08:00","labels":["stage:idea"]}
205+
{"id":"ge-okh","title":"Add npm script + docs to run embedding integration test","description":"Add an npm script to run the real-model embedding integration test (EMBED_NODE=1). Include a short note in web/demo/README.md describing the script and how to run it locally.\\n\\nAcceptance criteria:\\n- package.json has script that runs: \\n- web/demo/README.md contains a 1-2 line note explaining the script and env flags\\n- Tests: running the script locally succeeds (developer responsibility)\\n\\nFiles to be changed: , ","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-17T20:47:17.395948499-08:00","created_by":"rgardler","updated_at":"2026-01-17T20:47:17.395948499-08:00","labels":["stage:idea"],"dependencies":[{"issue_id":"ge-okh","depends_on_id":"ge-hch.5","type":"parent-child","created_at":"2026-01-19T23:01:36.648476327-08:00","created_by":"Ross Gardler"}]}
206206
{"id":"ge-oow","title":"Refactor: remove jq fallback for replay failure JSON","description":"### Goal\\nRemove the -based fallback in that synthesizes failure JSON from logs. The replay runner now writes structured JSON via ; the workflow should rely on that structured output instead of reconstructing it from logs.\\n\\n### Acceptance Criteria\\n- The workflow no longer runs to synthesize .\\n- The workflow copies or uploads the runner-produced (or a runner-produced failure file) into artifacts/results and archives it for failing runs.\\n- A CI run for the PR demonstrates a failing replay produces an uploaded visible in the job artifacts.\\n- Files touched are limited to (and any small adjustments to only if strictly necessary).\\n\\n### Suggested Implementation\\n1. Edit to remove the fallback block and instead rely on produced by the runner.\\n2. Ensure the workflow still copies raw logs and uploads \u0026 .\\n3. Run CI on a PR that intentionally fails a replay to confirm artifact presence.\\n\\n### Timebox\\nEstimate: 1-2 hours.\\n\\n### Notes\\n- Keep an eye on edge cases where runner result is missing; if this proves to happen, we may want a minimal guard that reports a clear error but does not attempt to reconstruct the JSON.\\n\\n### Related\\ndiscovered-from:ge-hch.4.3\\n","status":"closed","priority":3,"issue_type":"task","assignee":"rgardler","created_at":"2026-01-16T00:32:00.440882328-08:00","created_by":"rgardler","updated_at":"2026-01-16T01:38:44.850810587-08:00","closed_at":"2026-01-16T01:38:44.850810587-08:00","close_reason":"Completed: removed jq fallback; PR #151 merged","dependencies":[{"issue_id":"ge-oow","depends_on_id":"ge-hch.4.3","type":"discovered-from","created_at":"2026-01-16T00:32:00.453040701-08:00","created_by":"rgardler"}],"comments":[{"id":156,"issue_id":"ge-oow","author":"rgardler","text":"Created branch ge-oow/remove-jq-fallback and opened PR #151 to remove jq fallback; change uses printf to emit minimal failure JSON if runner result is missing. Marking as in_progress and assigned to rgardler.","created_at":"2026-01-16T09:34:53Z"},{"id":157,"issue_id":"ge-oow","author":"rgardler","text":"Merged PR #151: removed jq fallback and emit minimal failure JSON using printf. Verified replay artifacts show no failures for this change. Closing bead.","created_at":"2026-01-16T09:38:43Z"}]}
207207
{"id":"ge-osd","title":"Restore original demo story for smoke tests","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-01-06T22:09:37.056596959-08:00","created_by":"rgardler","updated_at":"2026-01-06T22:10:00.371743266-08:00","closed_at":"2026-01-06T22:10:00.371743266-08:00","close_reason":"Done","comments":[{"id":1,"issue_id":"ge-osd","author":"rgardler","text":"Fixed smoke tests broken by demo story changes by adding web/stories/test.ink (pre-903f044 demo story) and routing /stories/demo.ink to that file in Playwright.\n\nChanges:\n- web/stories/test.ink\n- tests/demo.smoke.spec.ts\n\nCommands:\n- npm test","created_at":"2026-01-07T06:09:54Z"}]}
208208
{"id":"ge-qip","title":"Parent: Test parent/child relationship","description":"escription","status":"tombstone","priority":2,"issue_type":"epic","owner":"ross@gardler.org","created_at":"2026-01-19T18:38:38.077369615-08:00","created_by":"Ross Gardler","updated_at":"2026-01-19T18:41:54.055362933-08:00","deleted_at":"2026-01-19T18:41:54.055362933-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"epic"}

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: 'bob@example.com' })
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: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 event = { type, timestamp: ts, payload: redact(payload) }
22+
this._push(event)
23+
for (const b of this.backends) {
24+
try { b.emit(event) } catch (e) { console.error('telemetry backend emit failed', e) }
25+
}
26+
}
27+
28+
_push(event) {
29+
this.events.push(event)
30+
if (this.events.length > this.bufferSize) this.events.shift()
31+
}
32+
33+
query(filterFn) {
34+
if (!filterFn) return this.events.slice()
35+
return this.events.filter(filterFn)
36+
}
37+
38+
clear() { this.events = [] }
39+
}
40+
41+
// Singleton for browser/demo usage
42+
const defaultTelemetry = new Telemetry()
43+
44+
module.exports = { Telemetry, defaultTelemetry }

src/telemetry/redact.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Minimal PII redaction utilities used by the telemetry emitter.
2+
'use strict'
3+
4+
const PII_KEY_RE = /(email|name|ssn|phone|address|credit|card|cc|token)/i
5+
const EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i
6+
7+
function isPlainObject(v) {
8+
return v && typeof v === 'object' && !Array.isArray(v)
9+
}
10+
11+
function redactValue(v) {
12+
if (typeof v !== 'string') return v
13+
if (EMAIL_RE.test(v)) return '[REDACTED_EMAIL]'
14+
// crude phone/credit detection
15+
if (/\b\d{3}[- ]?\d{2,4}[- ]?\d{2,4}\b/.test(v)) return '[REDACTED]'
16+
return v
17+
}
18+
19+
function redact(obj) {
20+
if (Array.isArray(obj)) return obj.map(redact)
21+
if (!isPlainObject(obj)) return redactValue(obj)
22+
23+
const out = {}
24+
for (const k of Object.keys(obj)) {
25+
const v = obj[k]
26+
if (PII_KEY_RE.test(k)) {
27+
out[k] = typeof v === 'string' ? redactValue(v) : '[REDACTED]'
28+
continue
29+
}
30+
if (Array.isArray(v)) out[k] = v.map(item => (isPlainObject(item) ? redact(item) : redactValue(item)))
31+
else if (isPlainObject(v)) out[k] = redact(v)
32+
else out[k] = redactValue(v)
33+
}
34+
return out
35+
}
36+
37+
module.exports = { redact }

0 commit comments

Comments
 (0)