Skip to content

Perf/telemetry optimization - #177

Merged
AssafMiron merged 3 commits into
mainfrom
perf/telemetry-optimization
May 22, 2026
Merged

AssafMiron merged 3 commits into
mainfrom
perf/telemetry-optimization

Conversation

@AssafMiron

@AssafMiron AssafMiron commented May 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

Release Notes

  • Performance Improvements
    • Workflow status polling and file watcher background checks now use longer, more efficient intervals.
    • Polling automatically pauses when browser tabs are inactive to reduce unnecessary server requests.
    • Improved request logging with additional endpoint filtering for cleaner output.
    • Reduced console logging for high-frequency events.

Review Change Stack

- Caches global settings and app version on mount in App.tsx to avoid thousands of redundant GET calls on event arrivals.
- Intercepts and dispatches client-side telemetry events locally to Google Analytics without a roundtrip feedback loop.
- Added a broadcast bypass option { broadcast: false } to trackTelemetry for client-originated events.
- Suppressed noisy heartbeat logs in SharedEventSource to clean the browser console.
- Excluded telemetry POST and health GET calls from server request logging to clean the backend console.
- Successfully verified by passing all 57 backend tests and all 35 E2E Playwright tests.
@AssafMiron
AssafMiron requested a review from aviatam as a code owner May 22, 2026 17:15
@coderabbitai

coderabbitai Bot commented May 22, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR refactors telemetry broadcasting and optimizes background polling across the application. The backend now supports selective event broadcasting, the frontend centralizes telemetry processing with cached state, and multiple polling mechanisms reduce unnecessary activity when the browser tab is hidden.

Changes

Telemetry Broadcast Control and Background Polling Optimization

Layer / File(s) Summary
Backend telemetry broadcast control
node-backend/lib/telemetry/index.mjs, node-backend/server.mjs
trackTelemetry accepts options parameter to conditionally emit SSE broadcast. /api/telemetry/event handler passes { broadcast: false } to prevent re-broadcasting. Request logging suppresses these endpoints.
Frontend telemetry event processing with cached state
src/App.tsx
Introduces cached activeSettings and activeVersion variables, adds processTelemetryEvent handler for centralized event routing with GA initialization, and wires window event listeners for settings updates and local telemetry events.
Server-to-client telemetry event emission
src/api/server.ts, src/api/runtime.ts
telemetryApi.track emits productos:local-telemetry-event CustomEvent on window. Runtime SSE handler suppresses console logging for high-frequency event types via IGNORED_CONSOLE_EVENTS.
Hidden-tab-aware polling reduction
src/components/workflow/WorkflowList.tsx, src/hooks/useFileWatcherEvents.ts, src/hooks/useWorkspaceInit.ts
Polling now checks document.hidden and skips execution when tab is inactive. Intervals extended: WorkflowList 1s→60s, useFileWatcherEvents 5s→120s. Event-driven updates replace constant polling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • AIResearchFactory/productOS#173: Extends the existing telemetry implementation by changing trackTelemetry signature to support { broadcast: false } and updates POST /api/telemetry/event handling.

Suggested reviewers

  • aviatam

🐰 A telemetry tale, so neat and clean,
No noisy logs on the browser scene,
When tabs hide, the polling sleeps deep,
Cached settings and GA events to keep,
Broadcast control makes the flow pristine! 📊✨

🚥 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 'Perf/telemetry optimization' directly relates to the main changes across the changeset, which focus on optimizing telemetry handling (adding broadcast control, reducing polling frequencies, suppressing noisy logs) and improving overall performance.
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 perf/telemetry-optimization

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: 2

🧹 Nitpick comments (2)
src/hooks/useFileWatcherEvents.ts (1)

46-53: ⚡ Quick win

Add visibility-resume catch-up refresh.

On Line 48 and Line 53, skipping hidden-tab polling is good, but returning to the tab can leave stale data for up to 120s. Add a visibilitychange handler to trigger one immediate refresh when the tab becomes visible.

🤖 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/hooks/useFileWatcherEvents.ts` around lines 46 - 53, The polling block
skips work when document.hidden which can leave data stale on resume; add a
document.addEventListener('visibilitychange', ...) inside useFileWatcherEvents
to immediately call
appApi.getProjectWorkflows(activeProject.id).then(setWorkflows).catch(()=>{})
and appApi.listArtifacts(activeProject.id).then(setArtifacts).catch(()=>{}) when
the tab becomes visible (check document.visibilityState === 'visible' or
!document.hidden), and ensure you remove that listener in the hook cleanup (same
place you clearInterval) so you don't leak listeners; use the same handlers used
in the interval (setWorkflows/setArtifacts) and keep error handling consistent.
src/hooks/useWorkspaceInit.ts (1)

75-80: ⚡ Quick win

Trigger fallback refresh when tab becomes visible.

On Line 76, this can defer recovery refresh up to 5 minutes after tab restore. Consider a one-shot visibilitychange refresh when document.hidden becomes false.

🤖 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/hooks/useWorkspaceInit.ts` around lines 75 - 80, The periodic interval in
useWorkspaceInit skips refreshFallback while document.hidden and can delay
recovery up to 5 minutes; add a one-shot visibilitychange handler that, when
document.visibilityState becomes "visible" (or document.hidden === false),
checks appApi.isServerOnline() and calls refreshFallback().catch(() => {})
immediately, then removes itself; register this listener alongside the existing
interval and ensure it is cleaned up when the hook unmounts (clearInterval and
removeEventListener) so the one-shot runs only once per visibility transition.
🤖 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 `@src/App.tsx`:
- Around line 323-329: The handler handleSettingsChanged is trusting every
productos:settings-changed event and overwrites activeSettings even for
project-level updates; change it to only update activeSettings when the event
payload explicitly indicates global settings (e.g., check
customEvent.detail.scope === 'global' or a dedicated flag like
customEvent.detail.isGlobal === true or absence of a projectId), otherwise
ignore or log and return; update the conditional inside handleSettingsChanged to
guard on that property before assigning activeSettings and emitting telemetry,
and reference runtimeApi.saveProjectSettings as the source that emits
project-level payloads so you can differentiate project vs global payloads.

In `@src/components/workflow/WorkflowList.tsx`:
- Around line 53-63: The listener promise can resolve after cleanup leaving the
handler attached; introduce a boolean flag (e.g., disposed) in the component
scope, set disposed = true in the cleanup, and when the
appApi.listen<any>('workflow-progress', ...) promise resolves check the flag: if
disposed call the returned unlisten function immediately (u()), otherwise assign
it to the existing unlisten variable; ensure the same disposed check is used
before assigning/using unlisten in the return cleanup and keep pollRuns()
invocation unchanged.

---

Nitpick comments:
In `@src/hooks/useFileWatcherEvents.ts`:
- Around line 46-53: The polling block skips work when document.hidden which can
leave data stale on resume; add a document.addEventListener('visibilitychange',
...) inside useFileWatcherEvents to immediately call
appApi.getProjectWorkflows(activeProject.id).then(setWorkflows).catch(()=>{})
and appApi.listArtifacts(activeProject.id).then(setArtifacts).catch(()=>{}) when
the tab becomes visible (check document.visibilityState === 'visible' or
!document.hidden), and ensure you remove that listener in the hook cleanup (same
place you clearInterval) so you don't leak listeners; use the same handlers used
in the interval (setWorkflows/setArtifacts) and keep error handling consistent.

In `@src/hooks/useWorkspaceInit.ts`:
- Around line 75-80: The periodic interval in useWorkspaceInit skips
refreshFallback while document.hidden and can delay recovery up to 5 minutes;
add a one-shot visibilitychange handler that, when document.visibilityState
becomes "visible" (or document.hidden === false), checks appApi.isServerOnline()
and calls refreshFallback().catch(() => {}) immediately, then removes itself;
register this listener alongside the existing interval and ensure it is cleaned
up when the hook unmounts (clearInterval and removeEventListener) so the
one-shot runs only once per visibility transition.
🪄 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: 5db05990-84d9-4151-b340-28ee54ed2b7f

📥 Commits

Reviewing files that changed from the base of the PR and between 3a97707 and ce1b1aa.

📒 Files selected for processing (8)
  • node-backend/lib/telemetry/index.mjs
  • node-backend/server.mjs
  • src/App.tsx
  • src/api/runtime.ts
  • src/api/server.ts
  • src/components/workflow/WorkflowList.tsx
  • src/hooks/useFileWatcherEvents.ts
  • src/hooks/useWorkspaceInit.ts

Comment thread src/App.tsx
Comment on lines +323 to +329
const handleSettingsChanged = (e: Event) => {
const customEvent = e as CustomEvent;
if (customEvent.detail) {
console.log('[Telemetry] Active settings updated:', customEvent.detail);
activeSettings = customEvent.detail;
}
};

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard cache updates to global settings payload only.

Line 323-Line 329 currently trusts every productos:settings-changed payload as global settings. runtimeApi.saveProjectSettings dispatches the same event name with project settings, which can overwrite activeSettings and cause telemetry to be sent even after a global opt-out.

Proposed fix
-    const handleSettingsChanged = (e: Event) => {
-      const customEvent = e as CustomEvent;
-      if (customEvent.detail) {
-        console.log('[Telemetry] Active settings updated:', customEvent.detail);
-        activeSettings = customEvent.detail;
-      }
-    };
+    const handleSettingsChanged = (e: Event) => {
+      const customEvent = e as CustomEvent;
+      const detail = customEvent.detail;
+      if (detail && typeof detail === 'object' && 'telemetry' in detail) {
+        activeSettings = detail;
+      }
+    };
📝 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.

Suggested change
const handleSettingsChanged = (e: Event) => {
const customEvent = e as CustomEvent;
if (customEvent.detail) {
console.log('[Telemetry] Active settings updated:', customEvent.detail);
activeSettings = customEvent.detail;
}
};
const handleSettingsChanged = (e: Event) => {
const customEvent = e as CustomEvent;
const detail = customEvent.detail;
if (detail && typeof detail === 'object' && 'telemetry' in detail) {
activeSettings = detail;
}
};
🤖 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 323 - 329, The handler handleSettingsChanged is
trusting every productos:settings-changed event and overwrites activeSettings
even for project-level updates; change it to only update activeSettings when the
event payload explicitly indicates global settings (e.g., check
customEvent.detail.scope === 'global' or a dedicated flag like
customEvent.detail.isGlobal === true or absence of a projectId), otherwise
ignore or log and return; update the conditional inside handleSettingsChanged to
guard on that property before assigning activeSettings and emitting telemetry,
and reference runtimeApi.saveProjectSettings as the source that emits
project-level payloads so you can differentiate project vs global payloads.

Comment on lines +53 to +63
let unlisten: (() => void) | undefined;
appApi.listen<any>('workflow-progress', () => {
pollRuns();
}).then(u => {
unlisten = u;
}).catch(() => {});

return () => {
clearInterval(interval);
if (unlisten) unlisten();
};

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle async listener teardown race in cleanup.

On Line 53 and Line 60, unmount can happen before the listen promise resolves, leaving the listener attached. Add a disposed flag and unlisten immediately if registration finishes after cleanup.

Proposed fix
-        let unlisten: (() => void) | undefined;
+        let unlisten: (() => void) | undefined;
+        let disposed = false;
         appApi.listen<any>('workflow-progress', () => {
             pollRuns();
         }).then(u => {
-            unlisten = u;
+            if (disposed) {
+                u();
+            } else {
+                unlisten = u;
+            }
         }).catch(() => {});
 
         return () => {
+            disposed = true;
             clearInterval(interval);
             if (unlisten) unlisten();
         };
🤖 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/components/workflow/WorkflowList.tsx` around lines 53 - 63, The listener
promise can resolve after cleanup leaving the handler attached; introduce a
boolean flag (e.g., disposed) in the component scope, set disposed = true in the
cleanup, and when the appApi.listen<any>('workflow-progress', ...) promise
resolves check the flag: if disposed call the returned unlisten function
immediately (u()), otherwise assign it to the existing unlisten variable; ensure
the same disposed check is used before assigning/using unlisten in the return
cleanup and keep pollRuns() invocation unchanged.

@AssafMiron
AssafMiron merged commit 0c44b0f into main May 22, 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