Skip to content

feat: add privacy-safe usage telemetry - #173

Merged
aviatam merged 8 commits into
mainfrom
feat/usage-telemetry-analytics
May 21, 2026
Merged

aviatam merged 8 commits into
mainfrom
feat/usage-telemetry-analytics

Conversation

@aviatam

@aviatam aviatam commented May 21, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • add privacy-safe telemetry catalog, local install/session IDs, disk queue, opt-out, and optional endpoint flushing
  • instrument installation/onboarding, provider detection/selection, usage views, agent runs, projects, artifacts, skills, and workflows
  • add Settings privacy toggle and TELEMETRY.md documentation

Privacy safeguards

  • fixed event-name and per-event payload allowlists
  • strips unknown payload keys and caps string values
  • never sends prompts, outputs, file contents, file paths, project names, secrets, env vars, or stack traces
  • PRODUCTOS_TELEMETRY_DISABLED=1 short-circuits before telemetry I/O

Verification

  • node --test tests\\telemetry.test.mjs
  • node --check node-backend\\server.mjs; node --check node-backend\\lib\\workflows.mjs; node --check node-backend\\lib\\orchestrator.mjs; node --check node-backend\\lib\\telemetry\\index.mjs
  • npm run build

Summary by CodeRabbit

  • New Features

    • Anonymous usage analytics enabled by default with broad event coverage (onboarding, installation, provider selection, projects, artifacts, workflows, agent runs, chat, file/view actions) and an in-app “Privacy & Analytics” toggle to opt out; settings persisted.
    • Client-side queuing and forwarding to Google Analytics 4 for allowlisted events; events validated, normalized, and truncated to safe types/sizes.
  • Documentation

    • Added comprehensive telemetry docs describing collected fields, strict allowlists, persistence/queueing, and disablement.
  • Tests

    • Added tests for event sanitization, allowlists, and opt-out behavior.

Review Change Stack

@aviatam
aviatam requested a review from AssafMiron as a code owner May 21, 2026 04:56
@coderabbitai

coderabbitai Bot commented May 21, 2026 •

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e75ddca8-a012-4801-b7f1-59c46e51814d

📥 Commits

Reviewing files that changed from the base of the PR and between 8f25920 and b222bb2.

📒 Files selected for processing (5)
  • node-backend/lib/telemetry/catalog.mjs
  • src/App.tsx
  • src/components/workspace/ChatPanel.tsx
  • src/components/workspace/MarkdownEditor.tsx
  • src/pages/Workspace.tsx

📝 Walkthrough

Walkthrough

Adds a catalog-driven, sanitized telemetry pipeline: backend event catalog and sanitizer, in-process emitter and SSE forwarding, POST endpoint, frontend telemetry API and GA forwarding with a localStorage queue, instrumentation across onboarding/workflows/projects/artifacts/chat/files, UI toggle defaulting to enabled, types, docs, and tests.

Changes

Anonymous Telemetry End-to-End

Layer / File(s) Summary
Type contracts and documentation
src/api/contracts.ts, src/api/types.ts, src/api/app.ts, TELEMETRY.md
TelemetryPayloadValue and TelemetrySettings added; GlobalSettings extended with telemetry. Types re-exported; TELEMETRY.md documents allowlist, payload rules, queueing, GA behavior, and disable flow.
Frontend telemetry API and runtime wiring
src/api/server.ts, src/api/runtime.ts
Adds telemetryApi.track posting to /api/telemetry/event; runtimeApi.emit() forwards events via telemetryApi (errors ignored); runInstallation() now calls backend runner.
Onboarding & Installation instrumentation
src/pages/Onboarding.tsx, src/components/Installation/InstallationWizard.tsx
Emits onboarding.step on step changes, provider.selected when provider chosen, and onboarding.completed on successful setup (provider, starter-pack).
Workspace, chat, and file instrumentation
src/pages/Workspace.tsx, src/components/workspace/ChatPanel.tsx, src/components/workspace/MarkdownEditor.tsx
Tracks view.changed, chat.opened/chat.closed, chat.message_sent, and file.opened/file.closed/file.edited events tied to user interactions.
Settings UI, defaults, and user toggle
src/components/settings/SystemSettings.tsx, src/pages/GlobalSettings.tsx
Adds "Privacy & Analytics" switch bound to settings.telemetry?.enabled (default true), preserves other telemetry fields when updating, and initializes merged global settings with telemetry.enabled defaulting to true.
App-level GA initialization and event forwarding
src/App.tsx, package.json
Integrates react-ga4, lazily initializes GA based on global settings, listens for runtime telemetry events, forwards events to GA or enqueues them in localStorage when offline, and flushes queued events on reconnect or after init.
Backend catalog and core telemetry
node-backend/lib/telemetry/catalog.mjs, node-backend/lib/telemetry/index.mjs, tests/telemetry.test.mjs
Adds TELEMETRY_EVENTS and ALLOWED_EVENT_NAMES; implements sanitizeEvent, isTelemetryEnabled, normalization/truncation, telemetryEmitter, trackTelemetry, telemetryErrorCode, and tests for validation and normalization.
Server API, SSE wiring, and instrumentation
node-backend/server.mjs
Connects telemetryEmitter to SSE, adds POST /api/telemetry/event, and instruments installation, settings changes, usage, provider detection, chat/agent endpoints, projects, artifacts, workflows, skills, custom CLI, integrations, startup, and shutdown with telemetry emissions.
Orchestrator and workflows
node-backend/lib/orchestrator.mjs, node-backend/lib/workflows.mjs
Orchestrator emits artifact.created during agent loops; workflows emit agent.run.started, agent.run.completed, agent.run.failed, and workflow.completed with provider, duration, tokens, and error codes.
Telemetry tests
tests/telemetry.test.mjs
Adds unit tests covering unknown-event rejection, payload key allowlisting, value coercion/truncation, and telemetry enablement gating.

Sequence Diagram(s)

sequenceDiagram
  participant App as Application
  participant Runtime as runtimeApi
  participant Server as /api/telemetry/event
  participant Track as trackTelemetry()
  participant Emitter as telemetryEmitter

  App->>Runtime: runtimeApi.emit(event, payload)
  Runtime->>Server: POST /api/telemetry/event
  Server->>Track: trackTelemetry(name,payload,settings)
  Track->>Track: sanitizeEvent()
  Track->>Emitter: emit('event', {name,payload})
  Emitter->>App: SSE 'telemetry-event' (subscribers)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • AssafMiron

"🐰
I hop, I log, I queue with care,
Anonymous hops float through the air,
Users choose and toggles stay,
Events batched and sent away,
A tiny rabbit cheers—data fair! 🥕"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add privacy-safe usage telemetry' accurately and concisely summarizes the main change: introduction of a comprehensive, privacy-focused telemetry system with safeguards, opt-out capability, and event instrumentation throughout the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/usage-telemetry-analytics

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (3)
src/pages/GlobalSettings.tsx (1)

170-172: ⚡ Quick win

Preserve existing telemetry subfields when applying defaults.

This merge replaces loadedSettings.telemetry with { enabled }, which can silently discard other telemetry properties on next save. Merge the existing object first.

Suggested diff
-          telemetry: {
-            enabled: loadedSettings?.telemetry?.enabled ?? true,
-          },
+          telemetry: {
+            ...(loadedSettings?.telemetry || {}),
+            enabled: loadedSettings?.telemetry?.enabled ?? true,
+          },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/GlobalSettings.tsx` around lines 170 - 172, The code currently
replaces loadedSettings.telemetry with a new object containing only enabled,
which drops other telemetry subfields; change the assignment in
GlobalSettings.tsx to merge the existing telemetry object first (spread
loadedSettings?.telemetry or an empty object) and then set enabled using the
nullish fallback (enabled: loadedSettings?.telemetry?.enabled ?? true) so
existing properties are preserved while applying the default.
src/api/runtime.ts (2)

33-33: ⚡ Quick win

Use @/ alias for internal API imports in this file.

This changed import uses a relative path instead of the repo-standard alias.

♻️ Proposed fix
-import { SERVER_URL, serverOnline, serverFetch, systemApi, secretsApi, settingsApi, chatApi, authApi, projectsApi, projectsApiExtended, filesApi, artifactsApi, workflowsApi, skillsApi, mcpApi, researchLogApi, telemetryApi } from './server';
+import { SERVER_URL, serverOnline, serverFetch, systemApi, secretsApi, settingsApi, chatApi, authApi, projectsApi, projectsApiExtended, filesApi, artifactsApi, workflowsApi, skillsApi, mcpApi, researchLogApi, telemetryApi } from '`@/api/server`';
As per coding guidelines: `Use `@/` path alias for all internal module imports in React/TypeScript frontend code (maps to ./src/)`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/runtime.ts` at line 33, The import in runtime.ts uses a relative path
'./server' instead of the repo-standard alias; replace the relative import with
the '`@/`...' alias (e.g., import { SERVER_URL, serverOnline, serverFetch,
systemApi, secretsApi, settingsApi, chatApi, authApi, projectsApi,
projectsApiExtended, filesApi, artifactsApi, workflowsApi, skillsApi, mcpApi,
researchLogApi, telemetryApi } from '`@/api/server`') so all internal imports in
this file follow the alias convention and map to ./src/.

484-486: ⚡ Quick win

Make emit fire-and-forget to avoid accidental await latency.

Awaiting telemetry here can block callers on network/health-check delays. emit should stay best-effort and non-blocking.

♻️ Proposed fix
   async emit(event: string, payload?: any): Promise<void> {
-    await telemetryApi.track(event, payload || {});
+    void telemetryApi.track(event, payload ?? {});
   },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/runtime.ts` around lines 484 - 486, The emit method currently awaits
telemetryApi.track, blocking callers; change emit to fire-and-forget by invoking
telemetryApi.track(event, payload || {}) without awaiting it and attach a
.catch(...) to swallow/log errors to avoid unhandled promise rejections. Locate
the async emit(event: string, payload?: any): Promise<void> method and replace
the awaited call with a non-awaited call to telemetryApi.track(...) and add
minimal error handling (e.g., .catch(() => {}) or log via the existing logger)
so telemetry remains best-effort and non-blocking.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@node-backend/lib/orchestrator.mjs`:
- Around line 216-218: The telemetry call inside the artifact loop
(trackTelemetry used in runAgentLoop over artifactChanges) must be best-effort:
wrap the await trackTelemetry(...) call in a try/catch so any thrown error is
caught and does not propagate and fail runAgentLoop; on catch, log the error
(using the existing logger in scope, e.g., processLogger or console) with
context including the artifactType/change, but do not rethrow so artifact
processing remains successful.

In `@node-backend/lib/telemetry/index.mjs`:
- Around line 156-175: The flush logic (flushPromise) snapshots the queue with
readQueue() and later calls writeQueue(remaining), so events appended between
the initial read and the write can be lost; modify the flush flow to merge any
new events that arrived after the snapshot before writing the remaining items
back (e.g., after a successful POST, call readQueue() again and
writeQueue(concat(remaining, freshlyRead)) or otherwise perform a
compare-and-swap style update) and apply the same fix to the other queue-flush
area referenced (lines ~89-112); reference functions: flushPromise, readQueue,
writeQueue, and the batch/remaining logic to locate and update the code.
- Around line 164-171: The fetch call used to POST telemetry (the
fetch(endpoint, { method: 'POST', ... body: JSON.stringify({ events: batch })
})) has no timeout and can hang flush cycles (flushPromise); add an
AbortController with a setTimeout to cancel the request after a sensible timeout
(e.g. 5s), pass controller.signal into fetch, clear the timer on success, and
ensure the surrounding logic that resolves/rejects flushPromise correctly
handles an aborted/timeout error by treating it as a failed flush (log or
swallow as appropriate) so flushPromise cannot remain blocked indefinitely.

In `@node-backend/lib/workflows.mjs`:
- Around line 228-229: The awaited telemetry calls (trackTelemetry) must not be
allowed to throw and break workflow execution: wrap each await
trackTelemetry(...) in a try/catch that catches any error and logs it (or
silently handles it) without rethrowing so the workflow continues; apply this
pattern around the trackTelemetry call before invoking
orchestrator.runAgentLoop(...) and for the other trackTelemetry usages
referenced in this file (the similar calls near where orchestrator.runAgentLoop
and other agent step/finalization logic are executed) so telemetry failures
never affect workflow correctness.
- Around line 238-241: The telemetry call for "agent.run.completed" currently
treats any model_used value other than 'error' as success, which incorrectly
counts 'aborted' as successful; update the success logic used in the
trackTelemetry call (symbol: trackTelemetry, event: 'agent.run.completed',
property: result?.metadata?.model_used) so that 'aborted' is treated as a
failure (e.g., require model_used === 'success' or explicitly exclude both
'error' and 'aborted') and ensure undefined/null cases are handled consistently.
- Around line 253-257: The telemetry call for event 'agent.run.failed' currently
sends durationMs: 0; update it to send the actual elapsed time by computing
durationMs from the run's start timestamp or the error metadata: use a recorded
start time (e.g., runStartTime or workflowStartTimestamp) or the stepError
duration field (e.g., stepError.durationMs || stepError.duration) to compute
Date.now() - runStartTime, and pass that value to trackTelemetry instead of 0,
with a safe fallback to 0 if no timing data exists; keep the call to
trackTelemetry('agent.run.failed', {..., errorCode:
telemetryErrorCode(stepError)}, settings) intact.

In `@node-backend/server.mjs`:
- Line 1210: The startup call using readGlobalSettings().then(... ) can yield an
unhandled rejection if readGlobalSettings() fails; append a .catch(...) to that
promise chain to swallow or log the error and keep startup resilient (e.g., void
readGlobalSettings().then(settings => trackTelemetry('app.launched', {},
settings)).catch(err => { processLogger?.warn('Failed to read global settings
for telemetry', err) || console.warn('Failed to read global settings for
telemetry', err); })), referencing the existing readGlobalSettings and
trackTelemetry calls so the error is handled explicitly.

---

Nitpick comments:
In `@src/api/runtime.ts`:
- Line 33: The import in runtime.ts uses a relative path './server' instead of
the repo-standard alias; replace the relative import with the '`@/`...' alias
(e.g., import { SERVER_URL, serverOnline, serverFetch, systemApi, secretsApi,
settingsApi, chatApi, authApi, projectsApi, projectsApiExtended, filesApi,
artifactsApi, workflowsApi, skillsApi, mcpApi, researchLogApi, telemetryApi }
from '`@/api/server`') so all internal imports in this file follow the alias
convention and map to ./src/.
- Around line 484-486: The emit method currently awaits telemetryApi.track,
blocking callers; change emit to fire-and-forget by invoking
telemetryApi.track(event, payload || {}) without awaiting it and attach a
.catch(...) to swallow/log errors to avoid unhandled promise rejections. Locate
the async emit(event: string, payload?: any): Promise<void> method and replace
the awaited call with a non-awaited call to telemetryApi.track(...) and add
minimal error handling (e.g., .catch(() => {}) or log via the existing logger)
so telemetry remains best-effort and non-blocking.

In `@src/pages/GlobalSettings.tsx`:
- Around line 170-172: The code currently replaces loadedSettings.telemetry with
a new object containing only enabled, which drops other telemetry subfields;
change the assignment in GlobalSettings.tsx to merge the existing telemetry
object first (spread loadedSettings?.telemetry or an empty object) and then set
enabled using the nullish fallback (enabled: loadedSettings?.telemetry?.enabled
?? true) so existing properties are preserved while applying the default.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: fa632d4b-e27c-47c6-9dc6-bda19108a5b7

📥 Commits

Reviewing files that changed from the base of the PR and between b5f1b22 and 93e4d42.

📒 Files selected for processing (16)
  • TELEMETRY.md
  • node-backend/lib/orchestrator.mjs
  • node-backend/lib/telemetry/catalog.mjs
  • node-backend/lib/telemetry/index.mjs
  • node-backend/lib/workflows.mjs
  • node-backend/server.mjs
  • src/api/app.ts
  • src/api/contracts.ts
  • src/api/runtime.ts
  • src/api/server.ts
  • src/api/types.ts
  • src/components/Installation/InstallationWizard.tsx
  • src/components/settings/SystemSettings.tsx
  • src/pages/GlobalSettings.tsx
  • src/pages/Onboarding.tsx
  • tests/telemetry.test.mjs

Comment thread node-backend/lib/orchestrator.mjs
Comment thread node-backend/lib/telemetry/index.mjs Outdated
Comment thread node-backend/lib/telemetry/index.mjs Outdated
Comment thread node-backend/lib/workflows.mjs Outdated
Comment thread node-backend/lib/workflows.mjs Outdated
Comment thread node-backend/lib/workflows.mjs Outdated
Comment thread node-backend/server.mjs Outdated
Comment thread src/components/settings/SystemSettings.tsx Outdated
Comment thread src/components/settings/SystemSettings.tsx Outdated
Comment thread src/components/settings/SystemSettings.tsx Outdated
Comment thread TELEMETRY.md Outdated
Comment thread node-backend/lib/telemetry/catalog.mjs
Comment thread node-backend/lib/telemetry/catalog.mjs Outdated
Comment thread TELEMETRY.md
Comment thread TELEMETRY.md Outdated
Comment thread TELEMETRY.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/App.tsx (1)

144-144: ⚡ Quick win

Consider making the Google Analytics measurement ID configurable.

The GA measurement ID 'G-4K6J4VT5DR' is hardcoded on Line 144 (and duplicated on Line 153). For production flexibility and testing, consider moving this to an environment variable or configuration file.

♻️ Suggested refactor to use environment variable
-          ReactGA.initialize('G-4K6J4VT5DR');
+          const gaId = import.meta.env.VITE_GA_MEASUREMENT_ID || 'G-4K6J4VT5DR';
+          ReactGA.initialize(gaId);
           gaInitialized = true;

And similarly for Line 153.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/App.tsx` at line 144, The GA measurement ID is hardcoded in
ReactGA.initialize calls in App.tsx (both occurrences); change these to read
from a configurable environment variable (e.g.
process.env.REACT_APP_GA_MEASUREMENT_ID or your app's config) and use that
variable in both ReactGA.initialize calls instead of the literal 'G-4K6J4VT5DR';
also add a defensive check in the App component (or initAnalytics function) to
only call ReactGA.initialize when the environment variable is present and log or
skip initialization if it's missing so tests and non-production builds can opt
out.
node-backend/server.mjs (2)

556-593: ⚡ Quick win

provider.detected telemetry can be lost when authentication probes throw.

For Claude/Gemini/OpenAI detection, provider.checkAuthentication() is called between started and the track() call. If that probe throws (network, CLI error, missing config), the route returns 500 via the outer catch and the track('provider.detected', …) line is never reached — even though the CLI detection itself already produced a usable status.

Since success is derived from status.installed (not from authentication), consider either tracking before the auth probe or wrapping it so the telemetry fires regardless.

♻️ Example refactor (claude branch — same pattern applies to gemini/openai)
   if (req.method === 'GET' && url.pathname === '/api/system/detect/claude') {
     const started = Date.now();
     const settings = await readGlobalSettings();
     const status = await checkCli('claude');
     let authenticated = false;
-    if (status.installed) {
-      const provider = await AIService.createProvider('claudeCode', settings);
-      authenticated = await provider.checkAuthentication();
-    }
-    track('provider.detected', { provider: 'claudeCode', success: !!status.installed, durationMs: Date.now() - started }, settings);
+    try {
+      if (status.installed) {
+        const provider = await AIService.createProvider('claudeCode', settings);
+        authenticated = await provider.checkAuthentication();
+      }
+    } finally {
+      track('provider.detected', { provider: 'claudeCode', success: !!status.installed, durationMs: Date.now() - started }, settings);
+    }
     return sendJson(res, 200, { ...status, authenticated });
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node-backend/server.mjs` around lines 556 - 593, The telemetry call
track('provider.detected', ...) can be skipped if provider.checkAuthentication()
throws; to fix, ensure track is always called regardless of auth probe by either
moving the track(...) invocation to immediately after obtaining status (after
checkCli/resolveCliCommand) or by wrapping the authentication probe in a
try/catch: call AIService.createProvider(...) and run
provider.checkAuthentication() inside a try block that falls back to
authenticated = false on error (and optionally log), then always call
track('provider.detected', { provider: '<name>', success: !!status.installed,
durationMs: Date.now() - started }, settings) before returning sendJson(...);
apply this change to the Claude (checkCli('claude') / 'claudeCode'), Gemini
(checkCli('gemini') / 'geminiCli') and OpenAI
(resolveCliCommand('codex','openai') / 'openAiCli') branches so telemetry cannot
be lost.

487-492: 💤 Low value

Consider basic protection for the public /api/telemetry/event endpoint.

This endpoint accepts arbitrary client POSTs and writes to queue.ndjson for every accepted event. sanitizeEvent in trackTelemetry rejects unknown event names and strips unknown keys, so PII leakage is well-guarded. However, there is no rate limit and no body-size cap visible here, so a misbehaving page on a hostile origin (CORS is wildcard-open for SSE) could still flood the local queue or wedge disk I/O with rapid POSTs.

At minimum, consider a lightweight per-process rate limit on this route and/or asserting Content-Length before reading the body.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node-backend/server.mjs` around lines 487 - 492, The telemetry POST handler
for '/api/telemetry/event' currently reads the entire body and forwards events
to trackTelemetry (which writes queue.ndjson) without any size or rate
protections; add a lightweight per-process rate limiter and a
Content-Length/body-size guard before calling readJson to prevent flooding.
Concretely, in the POST handler (the block using req.method, url.pathname,
readJson, trackTelemetry, sendError, sendNoContent, and readGlobalSettings)
check req.headers['content-length'] and reject (400) if missing or exceeds a
reasonable cap (e.g., a few KB), and implement a simple in-memory token-bucket
or fixed-window counter keyed to a single global bucket that rejects requests
(429) when the rate (e.g., requests per second/minute) is exceeded; only call
readJson and trackTelemetry after these checks pass. Ensure error responses use
sendError/sendNoContent as currently used.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@TELEMETRY.md`:
- Around line 69-72: Update TELEMETRY.md to explicitly clarify the default
behavior of PRODUCTOS_TELEMETRY_ENDPOINT and its relationship to GA4: state that
PRODUCTOS_TELEMETRY_ENDPOINT default is unset (no network sends, events queued
locally and retried) and that GA4 client-side telemetry (the GA4 integration
described earlier) is independent and still sends from the client to Google
Analytics unless client telemetry is disabled; also explain that the backend
telemetry endpoint is for server-side/internal telemetry ingestion (different
from client-facing GA4), and mention whether there is any default endpoint value
or placeholder (e.g., none/unset) so readers know to configure
PRODUCTOS_TELEMETRY_ENDPOINT when they want server-side forwarding.

---

Nitpick comments:
In `@node-backend/server.mjs`:
- Around line 556-593: The telemetry call track('provider.detected', ...) can be
skipped if provider.checkAuthentication() throws; to fix, ensure track is always
called regardless of auth probe by either moving the track(...) invocation to
immediately after obtaining status (after checkCli/resolveCliCommand) or by
wrapping the authentication probe in a try/catch: call
AIService.createProvider(...) and run provider.checkAuthentication() inside a
try block that falls back to authenticated = false on error (and optionally
log), then always call track('provider.detected', { provider: '<name>', success:
!!status.installed, durationMs: Date.now() - started }, settings) before
returning sendJson(...); apply this change to the Claude (checkCli('claude') /
'claudeCode'), Gemini (checkCli('gemini') / 'geminiCli') and OpenAI
(resolveCliCommand('codex','openai') / 'openAiCli') branches so telemetry cannot
be lost.
- Around line 487-492: The telemetry POST handler for '/api/telemetry/event'
currently reads the entire body and forwards events to trackTelemetry (which
writes queue.ndjson) without any size or rate protections; add a lightweight
per-process rate limiter and a Content-Length/body-size guard before calling
readJson to prevent flooding. Concretely, in the POST handler (the block using
req.method, url.pathname, readJson, trackTelemetry, sendError, sendNoContent,
and readGlobalSettings) check req.headers['content-length'] and reject (400) if
missing or exceeds a reasonable cap (e.g., a few KB), and implement a simple
in-memory token-bucket or fixed-window counter keyed to a single global bucket
that rejects requests (429) when the rate (e.g., requests per second/minute) is
exceeded; only call readJson and trackTelemetry after these checks pass. Ensure
error responses use sendError/sendNoContent as currently used.

In `@src/App.tsx`:
- Line 144: The GA measurement ID is hardcoded in ReactGA.initialize calls in
App.tsx (both occurrences); change these to read from a configurable environment
variable (e.g. process.env.REACT_APP_GA_MEASUREMENT_ID or your app's config) and
use that variable in both ReactGA.initialize calls instead of the literal
'G-4K6J4VT5DR'; also add a defensive check in the App component (or
initAnalytics function) to only call ReactGA.initialize when the environment
variable is present and log or skip initialization if it's missing so tests and
non-production builds can opt out.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4e870214-7c45-42f7-82a8-8f3d7982c5a4

📥 Commits

Reviewing files that changed from the base of the PR and between 93e4d42 and 7a891de.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • TELEMETRY.md
  • node-backend/lib/orchestrator.mjs
  • node-backend/lib/telemetry/catalog.mjs
  • node-backend/lib/telemetry/index.mjs
  • node-backend/lib/workflows.mjs
  • node-backend/server.mjs
  • package.json
  • src/App.tsx
  • src/api/runtime.ts
  • src/components/settings/SystemSettings.tsx
  • src/pages/GlobalSettings.tsx

Comment thread TELEMETRY.md Outdated
Comment on lines +69 to +72
POST $PRODUCTOS_TELEMETRY_ENDPOINT
```

If `PRODUCTOS_TELEMETRY_ENDPOINT` is not configured, ProductOS queues events locally but does not send network requests. Failed sends stay on disk and are retried later.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Clarify the default behavior for PRODUCTOS_TELEMETRY_ENDPOINT.

The documentation states that if PRODUCTOS_TELEMETRY_ENDPOINT is not configured, events are queued locally but never sent. This is privacy-friendly but may not align with the GA4 integration documented earlier (Lines 21-26), which suggests client-side telemetry is sent to Google Analytics regardless of backend endpoint configuration.

Consider clarifying:

  1. Is GA4 client-side telemetry independent of the backend endpoint?
  2. Does the backend endpoint serve a different purpose (e.g., internal analytics)?
  3. Should the docs mention the default endpoint value, if any?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@TELEMETRY.md` around lines 69 - 72, Update TELEMETRY.md to explicitly clarify
the default behavior of PRODUCTOS_TELEMETRY_ENDPOINT and its relationship to
GA4: state that PRODUCTOS_TELEMETRY_ENDPOINT default is unset (no network sends,
events queued locally and retried) and that GA4 client-side telemetry (the GA4
integration described earlier) is independent and still sends from the client to
Google Analytics unless client telemetry is disabled; also explain that the
backend telemetry endpoint is for server-side/internal telemetry ingestion
(different from client-facing GA4), and mention whether there is any default
endpoint value or placeholder (e.g., none/unset) so readers know to configure
PRODUCTOS_TELEMETRY_ENDPOINT when they want server-side forwarding.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
TELEMETRY.md (1)

7-12: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Doc claims about installId and env metadata don't match the current implementation.

The simplified node-backend/lib/telemetry/index.mjs no longer persists anything to disk (no telemetry/telemetry.json, no installId generation) and sanitizeEvent does not augment payloads with version, runtime, Node version, OS, or arch — it strips everything except the per-event allowlisted keys in TELEMETRY_EVENTS. Either reinstate the metadata in the emit path, or update the doc to reflect that events carry only the event name + per-event allowlisted payload.

Note that GA4 will automatically capture client tech metadata (browser/OS/country) — that part is already covered in the "Google Analytics Integration" section, but it's distinct from a backend-generated installId or Node runtime info.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@TELEMETRY.md` around lines 7 - 12, The docs claim telemetry includes a
persisted installId and backend-added env metadata, but the current
implementation in node-backend/lib/telemetry/index.mjs (notably sanitizeEvent
and TELEMETRY_EVENTS) strips all non-allowlisted keys and does not persist
installId; update either the code or the docs. To fix: either (A) reinstate
metadata by generating/persisting an installId and augmenting events in the emit
path before sanitizeEvent (identify the emit/send function in index.mjs and add
installId, ProductOS/version, runtime/node version, OS, arch) and ensure
persistence to telemetry/telemetry.json, or (B) update TELEMETRY.md to remove
claims about installId and backend-generated env metadata and state that events
only contain event name plus per-event allowlisted payload (keep GA4 note
separate). Reference sanitizeEvent, TELEMETRY_EVENTS, and the emit/send function
in node-backend/lib/telemetry/index.mjs when making the change.
🧹 Nitpick comments (1)
src/App.tsx (1)

200-227: ⚡ Quick win

Avoid an appApi.getGlobalSettings() round-trip per telemetry event.

Every telemetry-event from the backend triggers a fresh appApi.getGlobalSettings() call before forwarding to GA. For chatty events (e.g. installation.status_checked, provider.detected, agent.run.*), this adds an HTTP round-trip per event on the UI thread. Cache the enabled flag in a closure variable and refresh it only when telemetry settings actually change — e.g. by listening for the settings.telemetry_changed telemetry-event you already emit on the backend, or by reading settings once in setupGA and refreshing in the Settings save flow.

♻️ Sketch
   useEffect(() => {
     let unlisten: (() => void) | undefined;
     let gaInitialized = false;
+    let telemetryEnabled = true;
     const GA_QUEUE_KEY = 'productos_telemetry_queue';
@@
     const setupGA = async () => {
       try {
         const settings = await appApi.getGlobalSettings();
-        if (settings.telemetry?.enabled !== false) {
+        telemetryEnabled = settings.telemetry?.enabled !== false;
+        if (telemetryEnabled) {
           ReactGA.initialize('G-5L4YKT4HJV');
           gaInitialized = true;
           await flushQueue();
         }

         unlisten = await runtimeApi.listen<any>('telemetry-event', async (event) => {
           try {
-            const currentSettings = await appApi.getGlobalSettings();
-            if (currentSettings.telemetry?.enabled !== false) {
+            const { event: name, payload } = event.payload;
+            if (name === 'settings.telemetry_changed' && typeof payload?.enabled === 'boolean') {
+              telemetryEnabled = payload.enabled;
+            }
+            if (telemetryEnabled) {
               if (!gaInitialized) {
                 ReactGA.initialize('G-5L4YKT4HJV');
                 gaInitialized = true;
+                await flushQueue();
               }
-              const { event: name, payload } = event.payload;
               if (navigator.onLine) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/App.tsx` around lines 200 - 227, The telemetry handler currently calls
appApi.getGlobalSettings() on every runtimeApi.listen('telemetry-event')
invocation; instead, introduce a cached boolean (e.g., telemetryEnabled)
initialized once (read from appApi.getGlobalSettings() in setupGA or at
component mount) and referenced inside the runtimeApi.listen callback (replace
current appApi.getGlobalSettings() usage), and update that cached value only
when telemetry settings change by subscribing to the
'settings.telemetry_changed' event (or by toggling it explicitly in the Settings
save flow); keep existing symbols like runtimeApi.listen('telemetry-event'),
gaInitialized, ReactGA.initialize and queueEvent unchanged, but remove the
per-event getGlobalSettings() round-trip and use the cached telemetryEnabled
flag inside the handler.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@TELEMETRY.md`:
- Around line 7-12: The docs claim telemetry includes a persisted installId and
backend-added env metadata, but the current implementation in
node-backend/lib/telemetry/index.mjs (notably sanitizeEvent and
TELEMETRY_EVENTS) strips all non-allowlisted keys and does not persist
installId; update either the code or the docs. To fix: either (A) reinstate
metadata by generating/persisting an installId and augmenting events in the emit
path before sanitizeEvent (identify the emit/send function in index.mjs and add
installId, ProductOS/version, runtime/node version, OS, arch) and ensure
persistence to telemetry/telemetry.json, or (B) update TELEMETRY.md to remove
claims about installId and backend-generated env metadata and state that events
only contain event name plus per-event allowlisted payload (keep GA4 note
separate). Reference sanitizeEvent, TELEMETRY_EVENTS, and the emit/send function
in node-backend/lib/telemetry/index.mjs when making the change.

---

Nitpick comments:
In `@src/App.tsx`:
- Around line 200-227: The telemetry handler currently calls
appApi.getGlobalSettings() on every runtimeApi.listen('telemetry-event')
invocation; instead, introduce a cached boolean (e.g., telemetryEnabled)
initialized once (read from appApi.getGlobalSettings() in setupGA or at
component mount) and referenced inside the runtimeApi.listen callback (replace
current appApi.getGlobalSettings() usage), and update that cached value only
when telemetry settings change by subscribing to the
'settings.telemetry_changed' event (or by toggling it explicitly in the Settings
save flow); keep existing symbols like runtimeApi.listen('telemetry-event'),
gaInitialized, ReactGA.initialize and queueEvent unchanged, but remove the
per-event getGlobalSettings() round-trip and use the cached telemetryEnabled
flag inside the handler.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0f90a555-787f-4f78-9ef0-815e9064a9cb

📥 Commits

Reviewing files that changed from the base of the PR and between 7a891de and 8f25920.

📒 Files selected for processing (4)
  • TELEMETRY.md
  • node-backend/lib/telemetry/index.mjs
  • node-backend/server.mjs
  • src/App.tsx

…nt telemetry

- Dynamically detect client OS and browser in GA4 configuration

- Ingest global app version via appApi.getAppVersion()

- Capture pageview hits on tab/document view changes

- Trigger anonymous, privacy-compliant events: view.changed, file.opened, file.closed, file.edited, chat.message_sent, chat.opened, chat.closed
@aviatam
aviatam merged commit 5edf4cb into main May 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants