feat(telemetry): implement storage layer (Phase 1)#20
Conversation
Add StorageBackend interface with append(), query(), count() methods. Implement MemoryStorageBackend (in-process array, ephemeral) and FileStorageBackend (JSONL append-only, lazy dir creation, graceful handling of missing files and corrupted lines). 30 storage tests covering shared contract (both backends) plus backend-specific behavior (persistence, JSONL format, nested dirs). Closes tasks 1.1-1.4 in telemetry ROADMAP.
📝 WalkthroughWalkthroughAdds a telemetry storage layer: a Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant Telemetry
participant StorageBackend
participant FileSystem
Client->>Telemetry: createTelemetry(config)
Telemetry->>StorageBackend: instantiate (Memory|File) backend
alt File backend (on first append)
StorageBackend->>FileSystem: mkdir(fileDir) [lazy on first append]
end
Client->>Telemetry: recordEvent(event)
Telemetry->>StorageBackend: append(event)
alt File backend
StorageBackend->>FileSystem: write(JSONL line)
FileSystem-->>StorageBackend: ack
else Memory backend
StorageBackend-->>Telemetry: resolved
end
Client->>Telemetry: queryEvents(filter)
Telemetry->>StorageBackend: query(filter)
alt File backend
StorageBackend->>FileSystem: read(file)
FileSystem-->>StorageBackend: file content
end
StorageBackend->>StorageBackend: parse lines & matchesFilter(event,filter)
StorageBackend-->>Telemetry: matching events[]
Telemetry-->>Client: results
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
View your CI Pipeline Execution ↗ for commit b16ecd7
☁️ Nx Cloud last updated this comment at |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 41953f865e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| async count(filter: EventFilter): Promise<number> { | ||
| const events = await this.readAll(); | ||
| return events.filter((e) => matchesFilter(e, filter)).length; |
There was a problem hiding this comment.
Stream file when counting matching events
count() currently loads and deserializes the entire JSONL file via readAll() before returning a number, so large telemetry logs can cause avoidable memory spikes and slowdowns for a simple count operation. This is especially problematic when callers use count() for lightweight checks, because it pays the same full-read cost as query(); scan line-by-line and increment matches instead of materializing all events.
Useful? React with 👍 / 👎.
| async append(event: PipelineEvent): Promise<void> { | ||
| this.events.push(event); |
There was a problem hiding this comment.
Snapshot appended events in memory storage
append() stores the caller's PipelineEvent object by reference, so if that object is mutated later (for example, reused across stages), previously stored telemetry is silently rewritten. This creates incorrect historical data and inconsistent behavior versus FileStorageBackend, which snapshots event data at write time; persist a cloned snapshot instead of the original reference.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (4)
openspec/specs/memory-storage/spec.md (1)
5-5: TODO placeholder in Purpose section.The Purpose section contains a placeholder asking to be updated after archiving. Consider updating it to describe the role of
MemoryStorageBackendin the telemetry system.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@openspec/specs/memory-storage/spec.md` at line 5, Replace the "TBD" placeholder in the Purpose section with a concise description of the MemoryStorageBackend: state that MemoryStorageBackend is an in-memory telemetry storage implementation used for short-lived telemetry buffer and testing, describe its responsibilities (storing telemetry events/metrics, indexing/querying, lifecycle and retention semantics), note that it is not durable/persistent and is intended to be used before/alongside archival processes (e.g., telemetry-phase-1 archive), and call out supported data types and any performance/size constraints so readers understand when to use it versus the archive backend.packages/telemetry/src/storage/FileStorageBackend.ts (3)
50-58:matchesFilterfunction is duplicated across both backends.This function is identical to the one in
MemoryStorageBackend.ts(lines 20-28). Extract it to a shared utility to follow DRY principles.Suggested approach
Create a shared filter utility, e.g.,
src/storage/filterUtils.ts:import type { PipelineEvent, EventFilter } from '../types.js'; export function matchesFilter(event: PipelineEvent, filter: EventFilter): boolean { if (event.pipelineId !== filter.pipelineId) return false; if (filter.stage !== undefined && event.stage !== filter.stage) return false; if (filter.action !== undefined && event.action !== filter.action) return false; if (filter.from !== undefined && event.timestamp < filter.from) return false; if (filter.to !== undefined && event.timestamp > filter.to) return false; return true; }Then import and use in both backends.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/telemetry/src/storage/FileStorageBackend.ts` around lines 50 - 58, The matchesFilter function is duplicated in FileStorageBackend.ts and MemoryStorageBackend.ts; extract it into a shared utility (e.g., create src/storage/filterUtils.ts) exporting matchesFilter(event: PipelineEvent, filter: EventFilter) and then replace the local implementations in both FileStorageBackend and MemoryStorageBackend with imports from that utility; ensure you import the same types (PipelineEvent, EventFilter) and update references to call the shared matchesFilter function.
13-16:mkdircalled on everyappend()is inefficient but safe.Calling
mkdir({ recursive: true })on every append works correctly (it's a no-op if the directory exists), but adds unnecessary filesystem overhead.An optional optimization would be to track directory creation with a flag:
Optional optimization
export class FileStorageBackend implements StorageBackend { private readonly filePath: string; + private dirCreated = false; constructor(filePath: string) { this.filePath = filePath; } async append(event: PipelineEvent): Promise<void> { - await mkdir(dirname(this.filePath), { recursive: true }); + if (!this.dirCreated) { + await mkdir(dirname(this.filePath), { recursive: true }); + this.dirCreated = true; + } await appendFile(this.filePath, JSON.stringify(event) + '\n', 'utf-8'); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/telemetry/src/storage/FileStorageBackend.ts` around lines 13 - 16, The append method in FileStorageBackend calls mkdir(dirname(this.filePath), { recursive: true }) on every append which is inefficient; add a private boolean flag (e.g. this.dirCreated) to the FileStorageBackend class, set it false in the constructor, and in append() check the flag and only call mkdir the first time (await mkdir(...) then set this.dirCreated = true), falling back to calling mkdir on error or if the flag is false; ensure you still await appendFile(this.filePath, ...) afterwards and handle any mkdir errors appropriately.
23-26:count()loads all events into memory despite spec claiming efficiency.The spec (
openspec/specs/file-storage/spec.mdline 52) claimscount()should work "without requiring all events to be held in memory simultaneously," but this implementation callsreadAll()which loads everything.For Phase 1 this is acceptable, but consider noting this as a future optimization target for large datasets (e.g., streaming line-by-line with a counter).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/telemetry/src/storage/FileStorageBackend.ts` around lines 23 - 26, The current count() implementation in FileStorageBackend calls readAll() and therefore loads all events into memory; update count() to either (A) add a clear TODO note referencing the spec and mark it as a future optimization for large datasets, or (B) implement a memory-efficient streaming version that reads the storage file line-by-line (e.g., via a new streamReadLines or streamReadEvents helper) and increments a counter when matchesFilter(e, filter) is true without accumulating events; reference the existing count(), readAll(), and matchesFilter symbols and ensure the new approach preserves the same Promise<number> contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@HANDOFF.md`:
- Around line 9-23: The handoff doc shows Phase 1 as complete in the Current
State table but the Completed Work table and Next Phase Context still reference
Phase 1; update the Completed Work table to list Wave 1 / telemetry / Phase 1
with the appropriate title and date, change the Next Phase Context section to
target Phase 2 (adjust descriptions, goals, and any tasks currently describing
Phase 1), and update the "ROADMAP Status" and any other occurrences of "Phase 1"
to reflect completion (e.g., "Phase 1 complete" remains) and the upcoming target
"Phase 2". Ensure changes touch the "Current State" table, the "Completed Work"
table, and the "Next Phase Context" heading/content so the document is
internally consistent.
In `@openspec/changes/archive/2026-04-04-telemetry-phase-1/proposal.md`:
- Around line 26-27: Update the proposal doc to list the actual module names and
exported symbols used in the codebase: replace the incorrect module names with
the implemented module names and ensure the "Modified files" note reflects that
index exports the storage implementations; specifically reference the
implemented symbols StorageBackend, MemoryStorageBackend, and FileStorageBackend
so the documentation matches the actual exports in the code (and adjust the "New
files"/"Modified files" lines accordingly).
In
`@openspec/changes/archive/2026-04-04-telemetry-phase-1/specs/file-storage/spec.md`:
- Around line 44-46: The FileStorageBackend.count() currently calls readAll()
which deserializes the entire file into memory; change it to stream/iterate the
file line-by-line instead so only one event is deserialized at a time: replace
the readAll() usage in FileStorageBackend.count with a streaming/readline or
line-wise file read that skips empty/malformed lines, JSON.parse each line to a
PipelineEvent and apply matchesFilter(event, filter) to increment a counter,
handle ENOENT by treating file as empty and rethrow other errors; ensure no
array of events is built and malformed JSON lines are ignored.
In `@openspec/changes/archive/2026-04-04-telemetry-phase-1/tasks.md`:
- Around line 1-26: Update the task doc and code to match actual symbols and
correct the count allocation issue: rename or update the checklist entries to
reference src/storage/StorageBackend.ts, src/storage/MemoryStorageBackend.ts,
and src/storage/FileStorageBackend.ts and mark completed items with - [x] where
Phase 1 is done; ensure StorageBackend is exported from src/index.ts (symbol:
StorageBackend). In the MemoryStorageBackend and FileStorageBackend
implementations, replace any .filter().length usage in the count() method with a
non-allocating loop or reducer that increments a counter while evaluating the
same EventFilter predicate (reference method name count and classes
MemoryStorageBackend, FileStorageBackend). Also ensure file-based behaviors
described (append/query handling of missing file) match the checklist entries if
already implemented and mark those tasks done.
In `@openspec/specs/file-storage/spec.md`:
- Around line 50-52: The spec incorrectly mandates an implementation detail:
FileStorageBackend.count() currently calls readAll() (which materializes all
events), so either remove the absolute efficiency claim or change it to a
non-mandatory guidance and add a TODO for optimization; specifically, update the
requirement text so it no longer uses "SHALL count ... without requiring all
events to be held in memory simultaneously" (or weaken to "SHOULD" and add a
note to implement streaming later), or alternatively implement a
streaming/counting path in FileStorageBackend.count() that avoids readAll() by
iterating events via a streaming reader API (replace the readAll() call with a
read-stream/iterator-based approach) and document the behavior.
- Around line 13-16: The spec incorrectly requires FileStorageBackend to create
files and parent directories in the constructor; update the scenario to reflect
the actual lazy behavior by stating that FileStorageBackend creates the file and
parent directories on first use (i.e., when append() is first called) rather
than when the instance is instantiated; reference FileStorageBackend and its
append() behavior in the updated text.
In `@packages/telemetry/src/storage/FileStorageBackend.ts`:
- Around line 40-46: The code in FileStorageBackend that .flatMap's lines and
does JSON.parse has an empty catch that silently swallows parse errors; update
the catch in that parsing block (the JSON.parse(... ) as PipelineEvent) to
handle errors explicitly by logging a warning with the malformed line text and
the parse error (or re-throw with context if a logger is not available), then
continue to skip that line so behavior is unchanged; reference the
FileStorageBackend parsing block and the PipelineEvent parse to locate the
change.
In `@packages/telemetry/tests/storage.test.ts`:
- Line 1: The test file imports vitest helpers explicitly but omits afterEach;
update the import statement that currently lists describe, it, expect,
beforeEach to also include afterEach from 'vitest' so the file consistently uses
explicit imports for all vitest globals (ensure the import list includes
afterEach alongside describe, it, expect, beforeEach).
---
Nitpick comments:
In `@openspec/specs/memory-storage/spec.md`:
- Line 5: Replace the "TBD" placeholder in the Purpose section with a concise
description of the MemoryStorageBackend: state that MemoryStorageBackend is an
in-memory telemetry storage implementation used for short-lived telemetry buffer
and testing, describe its responsibilities (storing telemetry events/metrics,
indexing/querying, lifecycle and retention semantics), note that it is not
durable/persistent and is intended to be used before/alongside archival
processes (e.g., telemetry-phase-1 archive), and call out supported data types
and any performance/size constraints so readers understand when to use it versus
the archive backend.
In `@packages/telemetry/src/storage/FileStorageBackend.ts`:
- Around line 50-58: The matchesFilter function is duplicated in
FileStorageBackend.ts and MemoryStorageBackend.ts; extract it into a shared
utility (e.g., create src/storage/filterUtils.ts) exporting matchesFilter(event:
PipelineEvent, filter: EventFilter) and then replace the local implementations
in both FileStorageBackend and MemoryStorageBackend with imports from that
utility; ensure you import the same types (PipelineEvent, EventFilter) and
update references to call the shared matchesFilter function.
- Around line 13-16: The append method in FileStorageBackend calls
mkdir(dirname(this.filePath), { recursive: true }) on every append which is
inefficient; add a private boolean flag (e.g. this.dirCreated) to the
FileStorageBackend class, set it false in the constructor, and in append() check
the flag and only call mkdir the first time (await mkdir(...) then set
this.dirCreated = true), falling back to calling mkdir on error or if the flag
is false; ensure you still await appendFile(this.filePath, ...) afterwards and
handle any mkdir errors appropriately.
- Around line 23-26: The current count() implementation in FileStorageBackend
calls readAll() and therefore loads all events into memory; update count() to
either (A) add a clear TODO note referencing the spec and mark it as a future
optimization for large datasets, or (B) implement a memory-efficient streaming
version that reads the storage file line-by-line (e.g., via a new
streamReadLines or streamReadEvents helper) and increments a counter when
matchesFilter(e, filter) is true without accumulating events; reference the
existing count(), readAll(), and matchesFilter symbols and ensure the new
approach preserves the same Promise<number> contract.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b3bf56e6-da31-46ac-8c66-71cbab362d47
📒 Files selected for processing (18)
HANDOFF.mdopenspec/changes/archive/2026-04-04-telemetry-phase-1/.openspec.yamlopenspec/changes/archive/2026-04-04-telemetry-phase-1/README.mdopenspec/changes/archive/2026-04-04-telemetry-phase-1/design.mdopenspec/changes/archive/2026-04-04-telemetry-phase-1/proposal.mdopenspec/changes/archive/2026-04-04-telemetry-phase-1/specs/file-storage/spec.mdopenspec/changes/archive/2026-04-04-telemetry-phase-1/specs/memory-storage/spec.mdopenspec/changes/archive/2026-04-04-telemetry-phase-1/specs/storage-interface/spec.mdopenspec/changes/archive/2026-04-04-telemetry-phase-1/tasks.mdopenspec/specs/file-storage/spec.mdopenspec/specs/memory-storage/spec.mdopenspec/specs/storage-interface/spec.mdpackages/telemetry/ROADMAP.mdpackages/telemetry/src/index.tspackages/telemetry/src/storage/FileStorageBackend.tspackages/telemetry/src/storage/MemoryStorageBackend.tspackages/telemetry/src/storage/StorageBackend.tspackages/telemetry/tests/storage.test.ts
| ### Requirement: File storage count avoids full deserialization | ||
|
|
||
| The `FileStorageBackend.count()` method SHALL count matching events efficiently without requiring all events to be held in memory simultaneously. |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check FileStorageBackend.count() implementation
# Show the count() method implementation
ast-grep --pattern $'count($$$) {
$$$
}' packages/telemetry/src/storage/FileStorageBackend.ts
# Show if it calls readAll() which loads everything into memory
rg -A 5 'async count\(' packages/telemetry/src/storage/FileStorageBackend.tsRepository: pertrai1/open-forge
Length of output: 282
Fix count() to match spec requirement: avoid full deserialization.
The spec requires count() to avoid holding all events in memory simultaneously. The current implementation directly violates this by calling readAll(), which deserializes the entire file into a PipelineEvent[] array before filtering. For large telemetry files, this causes excessive memory usage.
Implement a streaming approach instead—process the file line-by-line, deserializing only to check the filter, without materializing the full event array:
async count(filter: EventFilter): Promise<number> {
const content = await readFile(this.filePath, 'utf-8').catch(err => {
if (isNodeError(err) && err.code === 'ENOENT') return '';
throw err;
});
let count = 0;
for (const line of content.split('\n')) {
if (line.trim() === '') continue;
try {
const event = JSON.parse(line) as PipelineEvent;
if (matchesFilter(event, filter)) count++;
} catch {
// Skip malformed lines
}
}
return count;
}For even better memory efficiency with very large files, consider using Node.js streams with readline to avoid loading the entire file as a string.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@openspec/changes/archive/2026-04-04-telemetry-phase-1/specs/file-storage/spec.md`
around lines 44 - 46, The FileStorageBackend.count() currently calls readAll()
which deserializes the entire file into memory; change it to stream/iterate the
file line-by-line instead so only one event is deserialized at a time: replace
the readAll() usage in FileStorageBackend.count with a streaming/readline or
line-wise file read that skips empty/malformed lines, JSON.parse each line to a
PipelineEvent and apply matchesFilter(event, filter) to increment a counter,
handle ENOENT by treating file as empty and rethrow other errors; ensure no
array of events is built and malformed JSON lines are ignored.
| .flatMap((line) => { | ||
| try { | ||
| return [JSON.parse(line) as PipelineEvent]; | ||
| } catch { | ||
| return []; | ||
| } | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Empty catch block violates coding guidelines.
The empty catch block silently swallows JSON parse errors. While skipping malformed lines is intentional, the coding guidelines state: "Do not use empty catch blocks; handle errors explicitly."
Consider logging the malformed line for debugging purposes:
Proposed fix
.flatMap((line) => {
try {
return [JSON.parse(line) as PipelineEvent];
- } catch {
+ } catch (err: unknown) {
+ // Skip malformed JSON lines - may occur from partial writes or corruption
return [];
}
});As per coding guidelines: "Do not use empty catch blocks; handle errors explicitly or re-throw with context"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/telemetry/src/storage/FileStorageBackend.ts` around lines 40 - 46,
The code in FileStorageBackend that .flatMap's lines and does JSON.parse has an
empty catch that silently swallows parse errors; update the catch in that
parsing block (the JSON.parse(... ) as PipelineEvent) to handle errors
explicitly by logging a warning with the malformed line text and the parse error
(or re-throw with context if a logger is not available), then continue to skip
that line so behavior is unchanged; reference the FileStorageBackend parsing
block and the PipelineEvent parse to locate the change.
- Extract shared matchesFilter utility from both storage backends - Add dirCreated flag to FileStorageBackend to skip redundant mkdir - Fix afterEach import in storage tests (explicit vitest import) - Correct filenames and task items in OpenSpec change artifacts - Fill Purpose fields in all three main specs - Weaken file-storage count() claim to match implementation - Fix constructor scenario to reflect lazy dir creation - Update HANDOFF.md with completed work and next phase context
The forge-loop was missing /opsx-verify and /opsx-sync steps between commit and archive, causing workflow violations. Also removed -y flag from archive call so the built-in sync assessment isn't auto-skipped. Deprecates packages/mcp/LOOP.md in favor of docs/LOOP.md.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.claude/commands/forge-loop.md:
- Line 205: The archive step currently logs a warning and continues on failure;
change the flow so the loop aborts when the archive command fails: locate the
Step 4 "archive" invocation in the forge-loop logic (the "archive command" call
in Step 4) and replace the "warn but continue" behavior with a strict failure
path that propagates the archive error (return non-zero / throw / exit the loop)
and prevents marking the phase/handoff/status as completed; ensure any status
updates or handoff/state transitions that occur after the archive call are gated
on a successful archive result so failures stop the loop and surface the error.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b71398f5-a3a7-40bd-b44d-2afbdf956909
📒 Files selected for processing (3)
.claude/commands/forge-loop.mddocs/LOOP.mdpackages/mcp/LOOP.md
✅ Files skipped from review due to trivial changes (1)
- packages/mcp/LOOP.md
| ``` | ||
|
|
||
| Warn but continue if archive fails. | ||
| The archive command includes its own sync assessment at Step 4 — review and confirm. Warn but continue if archive fails. |
There was a problem hiding this comment.
Do not continue loop when archive fails.
Line 205’s “warn but continue” risks marking a phase as completed in handoff/status while the change is still unarchived. This can create workflow drift and duplicate future work.
Suggested fix
-The archive command includes its own sync assessment at Step 4 — review and confirm. Warn but continue if archive fails.
+The archive command includes its own sync assessment at Step 4 — review and confirm.
+If archive fails, stop the loop for this phase, document the blocker in PIPELINE-ISSUES.md,
+and resume only after the archive issue is resolved.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| The archive command includes its own sync assessment at Step 4 — review and confirm. Warn but continue if archive fails. | |
| The archive command includes its own sync assessment at Step 4 — review and confirm. | |
| If archive fails, stop the loop for this phase, document the blocker in PIPELINE-ISSUES.md, | |
| and resume only after the archive issue is resolved. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.claude/commands/forge-loop.md at line 205, The archive step currently logs
a warning and continues on failure; change the flow so the loop aborts when the
archive command fails: locate the Step 4 "archive" invocation in the forge-loop
logic (the "archive command" call in Step 4) and replace the "warn but continue"
behavior with a strict failure path that propagates the archive error (return
non-zero / throw / exit the loop) and prevents marking the phase/handoff/status
as completed; ensure any status updates or handoff/state transitions that occur
after the archive call are gated on a successful archive result so failures stop
the loop and surface the error.
Summary
StorageBackendinterface withappend(),query(),count()methodsMemoryStorageBackend(in-process array, ephemeral) andFileStorageBackend(JSONL append-only, lazy dir creation, graceful missing-file handling)Details
First real implementation in
@open-forge/telemetry— Phase 0 was types-only. This storage layer is the persistence mechanism thatemit()andquery()(Phase 2) will compose.Design decisions (see archived openspec change
2026-04-04-telemetry-phase-1):append(), not on constructionQuality gates: build ✅ | 33 tests ✅ | lint ✅
Closes telemetry ROADMAP tasks 1.1–1.4.
Summary by CodeRabbit
New Features
Tests
Documentation
Docs/Workflow