mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-08 03:10:26 +00:00
parent
071c544960
commit
e367f68cf3
5 changed files with 192 additions and 7 deletions
|
|
@ -1,13 +1,15 @@
|
|||
{
|
||||
"timestamp": "2026-03-19T15:20:46.286203Z",
|
||||
"current_node": "preflight_lint",
|
||||
"timestamp": "2026-03-19T15:24:47.113617Z",
|
||||
"current_node": "implement",
|
||||
"completed_nodes": [
|
||||
"start",
|
||||
"toolchain",
|
||||
"preflight_compile",
|
||||
"preflight_lint"
|
||||
"preflight_lint",
|
||||
"implement"
|
||||
],
|
||||
"node_retries": {
|
||||
"implement": 1,
|
||||
"start": 1,
|
||||
"toolchain": 1,
|
||||
"preflight_lint": 1,
|
||||
|
|
@ -16,21 +18,26 @@
|
|||
"context_values": {
|
||||
"thread.toolchain.current_node": "preflight_compile",
|
||||
"internal.fidelity": "compact",
|
||||
"internal.thread_id": "preflight_compile",
|
||||
"internal.thread_id": "preflight_lint",
|
||||
"internal.retry_count.preflight_compile": 1,
|
||||
"command.output": "",
|
||||
"internal.retry_count.preflight_lint": 1,
|
||||
"last_response": "Here's a summary of all changes made:\n\n### `lib/crates/fabro-util/src/telemetry/sender.rs` — rewritten\n\n**Writer side:**\n- Renamed `send(track: Track)` → `emit(tracks: &[Track])` — accepts a sli",
|
||||
"graph.goal": "# Plan: JSONL analytics event file format\n\n## Context\n\nCurrently each CLI invocation writes a single `Track` event as a standalone JSON file (`~/.fabro/tmp/fabro-event-{uuid}.json`) and spawns a detached subprocess to send it. We want to switch to JSONL format (one JSON event per line) so a single file can contain multiple events. Filenames keep a UUID for uniqueness. This enables callers to batch multiple events into one file/subprocess.\n\nThe panic sender (`__send_panic`) is unaffected — it stays single-JSON-per-file.\n\n## Changes\n\n### 1. `lib/crates/fabro-util/src/telemetry/sender.rs` — rewrite\n\n**Writer — rename `send()` to `emit()`, accept multiple events:**\n- `pub fn emit(tracks: &[Track])` (was `pub fn send(track: Track)`)\n- Early return if `SEGMENT_WRITE_KEY` is `None` or `tracks` is empty\n- Generate a UUID for the filename: `fabro-events-{uuid}.jsonl`\n- Serialize each `Track` as a compact JSON line (`serde_json::to_string`), join with `\\n`\n- Pass the bytes to `spawn_fabro_subcommand(\"__send_analytics\", &filename, &json)` as before\n\nNo file locking needed — each invocation writes its own uniquely-named file.\n\n**Reader — rename `send_to_segment()` to `upload()`:**\n- Read file contents as string\n- Parse each non-empty line as `serde_json::Value`, inject `\"type\": \"track\"`, collect into batch array\n- Skip malformed lines with `tracing::warn!`\n- If no valid events, return `Ok(())`\n- POST to `https://api.segment.io/v1/batch` with payload `{\"batch\": [...]}`\n- Keep Basic auth the same\n\nExtract a pure `fn build_segment_batch(content: &str) -> Option<Value>` for testability.\n\n**Constants:**\n- Change `SEGMENT_API_URL` from `.../v1/track` to `.../v1/batch`\n\n### 2. `lib/crates/fabro-cli/src/main.rs`\n\n**`send_telemetry_event()` (~line 428):** Change call from `sender::send(track)` to `sender::emit(&[track])`.\n\n**`SendAnalytics` handler (~line 910):** Change call from `sender::send_to_segment(&path)` to `sender::upload(&path)`.\n\n### 3. `lib/crates/fabro-util/src/telemetry/spawn.rs` — no changes\n\n`spawn_fabro_subcommand` is generic (takes raw bytes). It continues to work for both JSONL analytics files and single-JSON panic files.\n\n### 4. No changes to these files\n- `event.rs` — `Track` struct unchanged\n- `panic.rs` — stays single-JSON-per-file\n- `mod.rs`, `anonymous_id.rs`, `context.rs`, `git.rs`, `sanitize.rs` — unchanged\n\n## Implementation order (red/green TDD)\n\nWrite each test first (red), then implement just enough to make it pass (green).\n\n### Step 1: `build_segment_batch` — pure function, no I/O\n\n1. **Red:** Write test `build_segment_batch_empty_content` — empty string returns `None`\n2. **Green:** Add `fn build_segment_batch(content: &str) -> Option<Value>` stub returning `None`\n3. **Red:** Write test `build_segment_batch_single_event` — one JSONL line produces `{\"batch\": [{\"type\": \"track\", ...}]}`\n4. **Green:** Implement line parsing, `\"type\": \"track\"` injection, batch wrapping\n5. **Red:** Write test `build_segment_batch_multiple_events` — two lines produce batch of 2\n6. **Green:** Should already pass\n7. **Red:** Write test `build_segment_batch_skips_malformed_lines` — one good + one bad line produces batch of 1\n8. **Green:** Add `continue` on parse error\n\n### Step 2: `emit()` — writer side\n\n9. **Red:** Update existing `send_noops_without_write_key` to use `emit(&[track])` signature\n10. **Green:** Rename `send` to `emit`, change signature to `&[Track]`, serialize as JSONL (one JSON line per track, joined with `\\n`), generate `fabro-events-{uuid}.jsonl` filename\n\n### Step 3: `upload()` — reader side\n\n11. **Red:** Write test `upload_noops_without_write_key` — same pattern as existing `send_panic_noops_without_dsn`\n12. **Green:** Rename `send_to_segment` to `upload`, change internals to read file as string, call `build_segment_batch`, POST to `/v1/batch`\n\n### Step 4: Wire up call sites in `main.rs`\n\n13. Update `send_telemetry_event()` to call `sender::emit(&[track])`\n14. Update `SendAnalytics` handler to call `sender::upload(&path)`\n\n### Step 5: Final checks\n\n```bash\ncargo fmt --check --all\ncargo clippy --workspace -- -D warnings\ncargo test -p fabro-util\ncargo test --workspace\n```\n",
|
||||
"current.preamble": "Goal: # Plan: JSONL analytics event file format\n\n## Context\n\nCurrently each CLI invocation writes a single `Track` event as a standalone JSON file (`~/.fabro/tmp/fabro-event-{uuid}.json`) and spawns a detached subprocess to send it. We want to switch to JSONL format (one JSON event per line) so a single file can contain multiple events. Filenames keep a UUID for uniqueness. This enables callers to batch multiple events into one file/subprocess.\n\nThe panic sender (`__send_panic`) is unaffected — it stays single-JSON-per-file.\n\n## Changes\n\n### 1. `lib/crates/fabro-util/src/telemetry/sender.rs` — rewrite\n\n**Writer — rename `send()` to `emit()`, accept multiple events:**\n- `pub fn emit(tracks: &[Track])` (was `pub fn send(track: Track)`)\n- Early return if `SEGMENT_WRITE_KEY` is `None` or `tracks` is empty\n- Generate a UUID for the filename: `fabro-events-{uuid}.jsonl`\n- Serialize each `Track` as a compact JSON line (`serde_json::to_string`), join with `\\n`\n- Pass the bytes to `spawn_fabro_subcommand(\"__send_analytics\", &filename, &json)` as before\n\nNo file locking needed — each invocation writes its own uniquely-named file.\n\n**Reader — rename `send_to_segment()` to `upload()`:**\n- Read file contents as string\n- Parse each non-empty line as `serde_json::Value`, inject `\"type\": \"track\"`, collect into batch array\n- Skip malformed lines with `tracing::warn!`\n- If no valid events, return `Ok(())`\n- POST to `https://api.segment.io/v1/batch` with payload `{\"batch\": [...]}`\n- Keep Basic auth the same\n\nExtract a pure `fn build_segment_batch(content: &str) -> Option<Value>` for testability.\n\n**Constants:**\n- Change `SEGMENT_API_URL` from `.../v1/track` to `.../v1/batch`\n\n### 2. `lib/crates/fabro-cli/src/main.rs`\n\n**`send_telemetry_event()` (~line 428):** Change call from `sender::send(track)` to `sender::emit(&[track])`.\n\n**`SendAnalytics` handler (~line 910):** Change call from `sender::send_to_segment(&path)` to `sender::upload(&path)`.\n\n### 3. `lib/crates/fabro-util/src/telemetry/spawn.rs` — no changes\n\n`spawn_fabro_subcommand` is generic (takes raw bytes). It continues to work for both JSONL analytics files and single-JSON panic files.\n\n### 4. No changes to these files\n- `event.rs` — `Track` struct unchanged\n- `panic.rs` — stays single-JSON-per-file\n- `mod.rs`, `anonymous_id.rs`, `context.rs`, `git.rs`, `sanitize.rs` — unchanged\n\n## Implementation order (red/green TDD)\n\nWrite each test first (red), then implement just enough to make it pass (green).\n\n### Step 1: `build_segment_batch` — pure function, no I/O\n\n1. **Red:** Write test `build_segment_batch_empty_content` — empty string returns `None`\n2. **Green:** Add `fn build_segment_batch(content: &str) -> Option<Value>` stub returning `None`\n3. **Red:** Write test `build_segment_batch_single_event` — one JSONL line produces `{\"batch\": [{\"type\": \"track\", ...}]}`\n4. **Green:** Implement line parsing, `\"type\": \"track\"` injection, batch wrapping\n5. **Red:** Write test `build_segment_batch_multiple_events` — two lines produce batch of 2\n6. **Green:** Should already pass\n7. **Red:** Write test `build_segment_batch_skips_malformed_lines` — one good + one bad line produces batch of 1\n8. **Green:** Add `continue` on parse error\n\n### Step 2: `emit()` — writer side\n\n9. **Red:** Update existing `send_noops_without_write_key` to use `emit(&[track])` signature\n10. **Green:** Rename `send` to `emit`, change signature to `&[Track]`, serialize as JSONL (one JSON line per track, joined with `\\n`), generate `fabro-events-{uuid}.jsonl` filename\n\n### Step 3: `upload()` — reader side\n\n11. **Red:** Write test `upload_noops_without_write_key` — same pattern as existing `send_panic_noops_without_dsn`\n12. **Green:** Rename `send_to_segment` to `upload`, change internals to read file as string, call `build_segment_batch`, POST to `/v1/batch`\n\n### Step 4: Wire up call sites in `main.rs`\n\n13. Update `send_telemetry_event()` to call `sender::emit(&[track])`\n14. Update `SendAnalytics` handler to call `sender::upload(&path)`\n\n### Step 5: Final checks\n\n```bash\ncargo fmt --check --all\ncargo clippy --workspace -- -D warnings\ncargo test -p fabro-util\ncargo test --workspace\n```\n\n\n## Completed stages\n- **toolchain**: success\n - Script: `command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1`\n - Stdout:\n ```\n cargo 1.94.0 (85eff7c80 2026-01-15)\n ```\n - Stderr: (empty)\n- **preflight_compile**: success\n - Script: `cargo check -q --workspace 2>&1`\n - Stdout: (empty)\n - Stderr: (empty)\n",
|
||||
"current.preamble": "Goal: # Plan: JSONL analytics event file format\n\n## Context\n\nCurrently each CLI invocation writes a single `Track` event as a standalone JSON file (`~/.fabro/tmp/fabro-event-{uuid}.json`) and spawns a detached subprocess to send it. We want to switch to JSONL format (one JSON event per line) so a single file can contain multiple events. Filenames keep a UUID for uniqueness. This enables callers to batch multiple events into one file/subprocess.\n\nThe panic sender (`__send_panic`) is unaffected — it stays single-JSON-per-file.\n\n## Changes\n\n### 1. `lib/crates/fabro-util/src/telemetry/sender.rs` — rewrite\n\n**Writer — rename `send()` to `emit()`, accept multiple events:**\n- `pub fn emit(tracks: &[Track])` (was `pub fn send(track: Track)`)\n- Early return if `SEGMENT_WRITE_KEY` is `None` or `tracks` is empty\n- Generate a UUID for the filename: `fabro-events-{uuid}.jsonl`\n- Serialize each `Track` as a compact JSON line (`serde_json::to_string`), join with `\\n`\n- Pass the bytes to `spawn_fabro_subcommand(\"__send_analytics\", &filename, &json)` as before\n\nNo file locking needed — each invocation writes its own uniquely-named file.\n\n**Reader — rename `send_to_segment()` to `upload()`:**\n- Read file contents as string\n- Parse each non-empty line as `serde_json::Value`, inject `\"type\": \"track\"`, collect into batch array\n- Skip malformed lines with `tracing::warn!`\n- If no valid events, return `Ok(())`\n- POST to `https://api.segment.io/v1/batch` with payload `{\"batch\": [...]}`\n- Keep Basic auth the same\n\nExtract a pure `fn build_segment_batch(content: &str) -> Option<Value>` for testability.\n\n**Constants:**\n- Change `SEGMENT_API_URL` from `.../v1/track` to `.../v1/batch`\n\n### 2. `lib/crates/fabro-cli/src/main.rs`\n\n**`send_telemetry_event()` (~line 428):** Change call from `sender::send(track)` to `sender::emit(&[track])`.\n\n**`SendAnalytics` handler (~line 910):** Change call from `sender::send_to_segment(&path)` to `sender::upload(&path)`.\n\n### 3. `lib/crates/fabro-util/src/telemetry/spawn.rs` — no changes\n\n`spawn_fabro_subcommand` is generic (takes raw bytes). It continues to work for both JSONL analytics files and single-JSON panic files.\n\n### 4. No changes to these files\n- `event.rs` — `Track` struct unchanged\n- `panic.rs` — stays single-JSON-per-file\n- `mod.rs`, `anonymous_id.rs`, `context.rs`, `git.rs`, `sanitize.rs` — unchanged\n\n## Implementation order (red/green TDD)\n\nWrite each test first (red), then implement just enough to make it pass (green).\n\n### Step 1: `build_segment_batch` — pure function, no I/O\n\n1. **Red:** Write test `build_segment_batch_empty_content` — empty string returns `None`\n2. **Green:** Add `fn build_segment_batch(content: &str) -> Option<Value>` stub returning `None`\n3. **Red:** Write test `build_segment_batch_single_event` — one JSONL line produces `{\"batch\": [{\"type\": \"track\", ...}]}`\n4. **Green:** Implement line parsing, `\"type\": \"track\"` injection, batch wrapping\n5. **Red:** Write test `build_segment_batch_multiple_events` — two lines produce batch of 2\n6. **Green:** Should already pass\n7. **Red:** Write test `build_segment_batch_skips_malformed_lines` — one good + one bad line produces batch of 1\n8. **Green:** Add `continue` on parse error\n\n### Step 2: `emit()` — writer side\n\n9. **Red:** Update existing `send_noops_without_write_key` to use `emit(&[track])` signature\n10. **Green:** Rename `send` to `emit`, change signature to `&[Track]`, serialize as JSONL (one JSON line per track, joined with `\\n`), generate `fabro-events-{uuid}.jsonl` filename\n\n### Step 3: `upload()` — reader side\n\n11. **Red:** Write test `upload_noops_without_write_key` — same pattern as existing `send_panic_noops_without_dsn`\n12. **Green:** Rename `send_to_segment` to `upload`, change internals to read file as string, call `build_segment_batch`, POST to `/v1/batch`\n\n### Step 4: Wire up call sites in `main.rs`\n\n13. Update `send_telemetry_event()` to call `sender::emit(&[track])`\n14. Update `SendAnalytics` handler to call `sender::upload(&path)`\n\n### Step 5: Final checks\n\n```bash\ncargo fmt --check --all\ncargo clippy --workspace -- -D warnings\ncargo test -p fabro-util\ncargo test --workspace\n```\n\n\n## Completed stages\n- **toolchain**: success\n - Script: `command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1`\n - Stdout:\n ```\n cargo 1.94.0 (85eff7c80 2026-01-15)\n ```\n - Stderr: (empty)\n- **preflight_compile**: success\n - Script: `cargo check -q --workspace 2>&1`\n - Stdout: (empty)\n - Stderr: (empty)\n- **preflight_lint**: success\n - Script: `cargo clippy -q --workspace -- -D warnings 2>&1`\n - Stdout: (empty)\n - Stderr: (empty)\n",
|
||||
"graph.model_stylesheet": "\n * { backend: api; model: claude-opus-4-6;}\n ",
|
||||
"failure_signature": "",
|
||||
"internal.node_visit_count": 1,
|
||||
"internal.retry_count.toolchain": 1,
|
||||
"command.stderr": "",
|
||||
"thread.preflight_lint.current_node": "implement",
|
||||
"response.implement": "Here's a summary of all changes made:\n\n### `lib/crates/fabro-util/src/telemetry/sender.rs` — rewritten\n\n**Writer side:**\n- Renamed `send(track: Track)` → `emit(tracks: &[Track])` — accepts a slice of events\n- Early returns if `SEGMENT_WRITE_KEY` is `None` or `tracks` is empty\n- `spawn_sender` serializes each `Track` as a compact JSON line, joins with `\\n`, generates a `fabro-events-{uuid}.jsonl` filename\n\n**Reader side:**\n- Renamed `send_to_segment()` → `upload()` — reads file as string, delegates to `build_segment_batch`, POSTs to `/v1/batch`\n- New pure function `build_segment_batch(content: &str) -> Option<Value>` — parses each non-empty JSONL line, injects `\"type\": \"track\"`, wraps in `{\"batch\": [...]}`, skips malformed lines with `tracing::warn!`\n\n**Constants:**\n- `SEGMENT_API_URL` changed from `.../v1/track` to `.../v1/batch`\n- Added `use uuid::Uuid` (removed unused `Track.message_id` dependency for filename)\n\n**Tests (9 total, all passing):**\n1. `build_segment_batch_empty_content` — empty string returns `None`\n2. `build_segment_batch_single_event` — one line produces batch with `\"type\": \"track\"`\n3. `build_segment_batch_multiple_events` — two lines produce batch of 2\n4. `build_segment_batch_skips_malformed_lines` — bad JSON lines are skipped\n5. `build_segment_batch_all_malformed` — all bad lines returns `None`\n6. `build_segment_batch_skips_blank_lines` — blank lines are ignored\n7. `emit_noops_without_write_key` — no-ops without compile-time key\n8. `emit_noops_with_empty_tracks` — no-ops on empty slice\n9. `upload_noops_without_write_key` — errors without compile-time key\n\n### `lib/crates/fabro-cli/src/main.rs` — two call sites updated\n\n- Line 429: `sender::send(track)` → `sender::emit(&[track])`\n- Line 911: `sender::send_to_segment(&path)` → `sender::upload(&path)`\n\n### Unchanged files (as planned)\n- `event.rs`, `panic.rs`, `mod.rs`, `spawn.rs`, `anonymous_id.rs`, `context.rs`, `git.rs`, `sanitize.rs` — no changes needed",
|
||||
"thread.preflight_compile.current_node": "preflight_lint",
|
||||
"internal.run_id": "01KM3AVN0Z99W7AX4PCVEB8C9J",
|
||||
"last_stage": "implement",
|
||||
"internal.retry_count.implement": 1,
|
||||
"graph.rankdir": "LR",
|
||||
"current_node": "preflight_lint",
|
||||
"current_node": "implement",
|
||||
"internal.retry_count.start": 1,
|
||||
"outcome": "success",
|
||||
"failure_class": "",
|
||||
|
|
@ -51,6 +58,29 @@
|
|||
"status": "success",
|
||||
"duration_ms": 0
|
||||
},
|
||||
"implement": {
|
||||
"status": "success",
|
||||
"context_updates": {
|
||||
"last_response": "Here's a summary of all changes made:\n\n### `lib/crates/fabro-util/src/telemetry/sender.rs` — rewritten\n\n**Writer side:**\n- Renamed `send(track: Track)` → `emit(tracks: &[Track])` — accepts a sli",
|
||||
"response.implement": "Here's a summary of all changes made:\n\n### `lib/crates/fabro-util/src/telemetry/sender.rs` — rewritten\n\n**Writer side:**\n- Renamed `send(track: Track)` → `emit(tracks: &[Track])` — accepts a slice of events\n- Early returns if `SEGMENT_WRITE_KEY` is `None` or `tracks` is empty\n- `spawn_sender` serializes each `Track` as a compact JSON line, joins with `\\n`, generates a `fabro-events-{uuid}.jsonl` filename\n\n**Reader side:**\n- Renamed `send_to_segment()` → `upload()` — reads file as string, delegates to `build_segment_batch`, POSTs to `/v1/batch`\n- New pure function `build_segment_batch(content: &str) -> Option<Value>` — parses each non-empty JSONL line, injects `\"type\": \"track\"`, wraps in `{\"batch\": [...]}`, skips malformed lines with `tracing::warn!`\n\n**Constants:**\n- `SEGMENT_API_URL` changed from `.../v1/track` to `.../v1/batch`\n- Added `use uuid::Uuid` (removed unused `Track.message_id` dependency for filename)\n\n**Tests (9 total, all passing):**\n1. `build_segment_batch_empty_content` — empty string returns `None`\n2. `build_segment_batch_single_event` — one line produces batch with `\"type\": \"track\"`\n3. `build_segment_batch_multiple_events` — two lines produce batch of 2\n4. `build_segment_batch_skips_malformed_lines` — bad JSON lines are skipped\n5. `build_segment_batch_all_malformed` — all bad lines returns `None`\n6. `build_segment_batch_skips_blank_lines` — blank lines are ignored\n7. `emit_noops_without_write_key` — no-ops without compile-time key\n8. `emit_noops_with_empty_tracks` — no-ops on empty slice\n9. `upload_noops_without_write_key` — errors without compile-time key\n\n### `lib/crates/fabro-cli/src/main.rs` — two call sites updated\n\n- Line 429: `sender::send(track)` → `sender::emit(&[track])`\n- Line 911: `sender::send_to_segment(&path)` → `sender::upload(&path)`\n\n### Unchanged files (as planned)\n- `event.rs`, `panic.rs`, `mod.rs`, `spawn.rs`, `anonymous_id.rs`, `context.rs`, `git.rs`, `sanitize.rs` — no changes needed",
|
||||
"last_stage": "implement"
|
||||
},
|
||||
"notes": "Stage completed: implement",
|
||||
"usage": {
|
||||
"model": "claude-opus-4-6",
|
||||
"input_tokens": 38607,
|
||||
"output_tokens": 4625,
|
||||
"cache_read_tokens": 268156,
|
||||
"cache_write_tokens": 40280,
|
||||
"reasoning_tokens": 113,
|
||||
"cost": 0.92598
|
||||
},
|
||||
"files_touched": [
|
||||
"/home/daytona/workspace/lib/crates/fabro-cli/src/main.rs",
|
||||
"/home/daytona/workspace/lib/crates/fabro-util/src/telemetry/sender.rs"
|
||||
],
|
||||
"duration_ms": 237748
|
||||
},
|
||||
"toolchain": {
|
||||
"status": "success",
|
||||
"context_updates": {
|
||||
|
|
@ -70,10 +100,11 @@
|
|||
"duration_ms": 71459
|
||||
}
|
||||
},
|
||||
"next_node_id": "implement",
|
||||
"next_node_id": "simplify_opus",
|
||||
"node_visits": {
|
||||
"start": 1,
|
||||
"preflight_lint": 1,
|
||||
"implement": 1,
|
||||
"toolchain": 1,
|
||||
"preflight_compile": 1
|
||||
}
|
||||
|
|
|
|||
108
nodes/implement/prompt.md
Normal file
108
nodes/implement/prompt.md
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
Goal: # Plan: JSONL analytics event file format
|
||||
|
||||
## Context
|
||||
|
||||
Currently each CLI invocation writes a single `Track` event as a standalone JSON file (`~/.fabro/tmp/fabro-event-{uuid}.json`) and spawns a detached subprocess to send it. We want to switch to JSONL format (one JSON event per line) so a single file can contain multiple events. Filenames keep a UUID for uniqueness. This enables callers to batch multiple events into one file/subprocess.
|
||||
|
||||
The panic sender (`__send_panic`) is unaffected — it stays single-JSON-per-file.
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. `lib/crates/fabro-util/src/telemetry/sender.rs` — rewrite
|
||||
|
||||
**Writer — rename `send()` to `emit()`, accept multiple events:**
|
||||
- `pub fn emit(tracks: &[Track])` (was `pub fn send(track: Track)`)
|
||||
- Early return if `SEGMENT_WRITE_KEY` is `None` or `tracks` is empty
|
||||
- Generate a UUID for the filename: `fabro-events-{uuid}.jsonl`
|
||||
- Serialize each `Track` as a compact JSON line (`serde_json::to_string`), join with `\n`
|
||||
- Pass the bytes to `spawn_fabro_subcommand("__send_analytics", &filename, &json)` as before
|
||||
|
||||
No file locking needed — each invocation writes its own uniquely-named file.
|
||||
|
||||
**Reader — rename `send_to_segment()` to `upload()`:**
|
||||
- Read file contents as string
|
||||
- Parse each non-empty line as `serde_json::Value`, inject `"type": "track"`, collect into batch array
|
||||
- Skip malformed lines with `tracing::warn!`
|
||||
- If no valid events, return `Ok(())`
|
||||
- POST to `https://api.segment.io/v1/batch` with payload `{"batch": [...]}`
|
||||
- Keep Basic auth the same
|
||||
|
||||
Extract a pure `fn build_segment_batch(content: &str) -> Option<Value>` for testability.
|
||||
|
||||
**Constants:**
|
||||
- Change `SEGMENT_API_URL` from `.../v1/track` to `.../v1/batch`
|
||||
|
||||
### 2. `lib/crates/fabro-cli/src/main.rs`
|
||||
|
||||
**`send_telemetry_event()` (~line 428):** Change call from `sender::send(track)` to `sender::emit(&[track])`.
|
||||
|
||||
**`SendAnalytics` handler (~line 910):** Change call from `sender::send_to_segment(&path)` to `sender::upload(&path)`.
|
||||
|
||||
### 3. `lib/crates/fabro-util/src/telemetry/spawn.rs` — no changes
|
||||
|
||||
`spawn_fabro_subcommand` is generic (takes raw bytes). It continues to work for both JSONL analytics files and single-JSON panic files.
|
||||
|
||||
### 4. No changes to these files
|
||||
- `event.rs` — `Track` struct unchanged
|
||||
- `panic.rs` — stays single-JSON-per-file
|
||||
- `mod.rs`, `anonymous_id.rs`, `context.rs`, `git.rs`, `sanitize.rs` — unchanged
|
||||
|
||||
## Implementation order (red/green TDD)
|
||||
|
||||
Write each test first (red), then implement just enough to make it pass (green).
|
||||
|
||||
### Step 1: `build_segment_batch` — pure function, no I/O
|
||||
|
||||
1. **Red:** Write test `build_segment_batch_empty_content` — empty string returns `None`
|
||||
2. **Green:** Add `fn build_segment_batch(content: &str) -> Option<Value>` stub returning `None`
|
||||
3. **Red:** Write test `build_segment_batch_single_event` — one JSONL line produces `{"batch": [{"type": "track", ...}]}`
|
||||
4. **Green:** Implement line parsing, `"type": "track"` injection, batch wrapping
|
||||
5. **Red:** Write test `build_segment_batch_multiple_events` — two lines produce batch of 2
|
||||
6. **Green:** Should already pass
|
||||
7. **Red:** Write test `build_segment_batch_skips_malformed_lines` — one good + one bad line produces batch of 1
|
||||
8. **Green:** Add `continue` on parse error
|
||||
|
||||
### Step 2: `emit()` — writer side
|
||||
|
||||
9. **Red:** Update existing `send_noops_without_write_key` to use `emit(&[track])` signature
|
||||
10. **Green:** Rename `send` to `emit`, change signature to `&[Track]`, serialize as JSONL (one JSON line per track, joined with `\n`), generate `fabro-events-{uuid}.jsonl` filename
|
||||
|
||||
### Step 3: `upload()` — reader side
|
||||
|
||||
11. **Red:** Write test `upload_noops_without_write_key` — same pattern as existing `send_panic_noops_without_dsn`
|
||||
12. **Green:** Rename `send_to_segment` to `upload`, change internals to read file as string, call `build_segment_batch`, POST to `/v1/batch`
|
||||
|
||||
### Step 4: Wire up call sites in `main.rs`
|
||||
|
||||
13. Update `send_telemetry_event()` to call `sender::emit(&[track])`
|
||||
14. Update `SendAnalytics` handler to call `sender::upload(&path)`
|
||||
|
||||
### Step 5: Final checks
|
||||
|
||||
```bash
|
||||
cargo fmt --check --all
|
||||
cargo clippy --workspace -- -D warnings
|
||||
cargo test -p fabro-util
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
|
||||
## Completed stages
|
||||
- **toolchain**: success
|
||||
- Script: `command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1`
|
||||
- Stdout:
|
||||
```
|
||||
cargo 1.94.0 (85eff7c80 2026-01-15)
|
||||
```
|
||||
- Stderr: (empty)
|
||||
- **preflight_compile**: success
|
||||
- Script: `cargo check -q --workspace 2>&1`
|
||||
- Stdout: (empty)
|
||||
- Stderr: (empty)
|
||||
- **preflight_lint**: success
|
||||
- Script: `cargo clippy -q --workspace -- -D warnings 2>&1`
|
||||
- Stdout: (empty)
|
||||
- Stderr: (empty)
|
||||
|
||||
|
||||
Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.
|
||||
5
nodes/implement/provider_used.json
Normal file
5
nodes/implement/provider_used.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"mode": "agent",
|
||||
"provider": "anthropic",
|
||||
"model": "claude-opus-4-6"
|
||||
}
|
||||
35
nodes/implement/response.md
Normal file
35
nodes/implement/response.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
Here's a summary of all changes made:
|
||||
|
||||
### `lib/crates/fabro-util/src/telemetry/sender.rs` — rewritten
|
||||
|
||||
**Writer side:**
|
||||
- Renamed `send(track: Track)` → `emit(tracks: &[Track])` — accepts a slice of events
|
||||
- Early returns if `SEGMENT_WRITE_KEY` is `None` or `tracks` is empty
|
||||
- `spawn_sender` serializes each `Track` as a compact JSON line, joins with `\n`, generates a `fabro-events-{uuid}.jsonl` filename
|
||||
|
||||
**Reader side:**
|
||||
- Renamed `send_to_segment()` → `upload()` — reads file as string, delegates to `build_segment_batch`, POSTs to `/v1/batch`
|
||||
- New pure function `build_segment_batch(content: &str) -> Option<Value>` — parses each non-empty JSONL line, injects `"type": "track"`, wraps in `{"batch": [...]}`, skips malformed lines with `tracing::warn!`
|
||||
|
||||
**Constants:**
|
||||
- `SEGMENT_API_URL` changed from `.../v1/track` to `.../v1/batch`
|
||||
- Added `use uuid::Uuid` (removed unused `Track.message_id` dependency for filename)
|
||||
|
||||
**Tests (9 total, all passing):**
|
||||
1. `build_segment_batch_empty_content` — empty string returns `None`
|
||||
2. `build_segment_batch_single_event` — one line produces batch with `"type": "track"`
|
||||
3. `build_segment_batch_multiple_events` — two lines produce batch of 2
|
||||
4. `build_segment_batch_skips_malformed_lines` — bad JSON lines are skipped
|
||||
5. `build_segment_batch_all_malformed` — all bad lines returns `None`
|
||||
6. `build_segment_batch_skips_blank_lines` — blank lines are ignored
|
||||
7. `emit_noops_without_write_key` — no-ops without compile-time key
|
||||
8. `emit_noops_with_empty_tracks` — no-ops on empty slice
|
||||
9. `upload_noops_without_write_key` — errors without compile-time key
|
||||
|
||||
### `lib/crates/fabro-cli/src/main.rs` — two call sites updated
|
||||
|
||||
- Line 429: `sender::send(track)` → `sender::emit(&[track])`
|
||||
- Line 911: `sender::send_to_segment(&path)` → `sender::upload(&path)`
|
||||
|
||||
### Unchanged files (as planned)
|
||||
- `event.rs`, `panic.rs`, `mod.rs`, `spawn.rs`, `anonymous_id.rs`, `context.rs`, `git.rs`, `sanitize.rs` — no changes needed
|
||||
6
nodes/implement/status.json
Normal file
6
nodes/implement/status.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"status": "success",
|
||||
"notes": "Stage completed: implement",
|
||||
"failure_reason": null,
|
||||
"timestamp": "2026-03-19T15:24:47.113112+00:00"
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue