Skip to content

fix: measure total response time instead of TTFB (#7594)#7628

Open
pkrisko wants to merge 4 commits intousebruno:mainfrom
pkrisko:fix/response-time-ttfb
Open

fix: measure total response time instead of TTFB (#7594)#7628
pkrisko wants to merge 4 commits intousebruno:mainfrom
pkrisko:fix/response-time-ttfb

Conversation

@pkrisko
Copy link
Copy Markdown

@pkrisko pkrisko commented Mar 31, 2026

Description

Fixes #7594.

Since #4472, request.responseType = 'stream' is set for all requests so SSE responses don't hang. A side-effect: Axios fires its response interceptor at TTFB (when headers arrive), not after the body is fully received. The interceptor sets request-duration at that point, so Bruno reports TTFB as "Response Time" — even when the body takes 30+ seconds to download.

For non-text/event-stream responses, responseTime is now re-measured after promisifyStream() resolves, capturing total end-to-end duration. SSE responses keep the interceptor's TTFB timing, which is appropriate for streams. Both the happy path and the error-response path are fixed.

I created a small server to reproduce the issue:

const http = require('http');
http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  setTimeout(() => { res.end(JSON.stringify({ done: true })); }, 5000);
}).listen(9999, () => console.log('Listening on :9999'));

And confirmed that we now show 5000ms instead of 0s.

image

Contribution Checklist:

  • I've used AI significantly to create this pull request
  • The pull request only addresses one issue or adds one feature.
  • The pull request does not introduce any breaking changes
  • I have added screenshots or gifs to help explain the change if applicable.
  • I have read the contribution guidelines.
  • Create an issue and link to the pull request.

Publishing to New Package Managers

Please see here for more information.

Summary by CodeRabbit

  • Bug Fixes

    • More accurate and consistent response-time measurement for both standard and streaming responses; duration metadata is now handled and cleaned up reliably.
  • Tests

    • Added comprehensive tests covering response-time computation and stream-handling utilities, including event-stream detection, chunk concatenation, early-close behavior, and error handling.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

Walkthrough

Centralized response-time computation into a new measureResponseTime helper, exported promisifyStream and hasStreamHeaders, and updated request and runner/collection success/error paths to use the new timing logic; added tests for the three helpers.

Changes

Cohort / File(s) Summary
Network IPC core
packages/bruno-electron/src/ipc/network/index.js
Added measureResponseTime(config, headers, isStream); replaced direct request-duration reads with measureResponseTime(...) in request success/error and runner/collection success/error paths; delete request-duration from response headers.
Helper exports
packages/bruno-electron/src/ipc/network/index.js
Exported promisifyStream and hasStreamHeaders alongside the new measureResponseTime.
Tests
packages/bruno-electron/tests/network/index.spec.js
Imported and added Jest suites for hasStreamHeaders, promisifyStream, and measureResponseTime validating stream detection, stream promise behavior (closeOnFirst, errors), and response-time calculation/fallbacks.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

⚙️ A tiny timer found its place,
Streams and headers set the pace,
Promises wait or jump and go,
Durations measured, clean and slow,
Tests applaud the steady grace.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing response time measurement to capture end-to-end duration instead of TTFB.
Linked Issues check ✅ Passed The changes directly address issue #7594 by re-measuring responseTime after stream completion for non-SSE responses, capturing full duration instead of TTFB.
Out of Scope Changes check ✅ Passed All changes are scoped to response time measurement logic and necessary test coverage; no unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/bruno-electron/src/ipc/network/index.js (1)

1626-1627: ⚠️ Potential issue | 🟠 Major

Inconsistent timing: collection runner still reports TTFB.

The run-collection-folder handler directly reads request-duration from headers, which was set by the axios interceptor at TTFB (when headers arrived). This bypasses the new measureResponseTime logic that recalculates total duration for non-stream responses.

The standalone request path (lines 919, 942) now correctly reports total time, but collection/folder runs will still show TTFB.

Consider applying the same pattern here:

Proposed fix
-              response.responseTime = response.headers.get('request-duration');
+              response.responseTime = measureResponseTime(response.config, response.headers, false);

And similarly at line 1664 for the error path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-electron/src/ipc/network/index.js` around lines 1626 - 1627,
The collection/folder runner reads request-duration directly from headers
(response.headers.get('request-duration')) which reflects TTFB and bypasses
measureResponseTime; update the run-collection-folder handler to call the same
measureResponseTime routine used by the standalone path (the function named
measureResponseTime) to compute total duration, set response.responseTime to
that computed value, then remove the request-duration header; apply the same
change in the error handling path (the block currently at the later error path)
so both success and error branches use measureResponseTime rather than the raw
header.
🧹 Nitpick comments (1)
packages/bruno-electron/tests/network/index.spec.js (1)

62-87: measureResponseTime tests cover the branching logic well.

However, the test at lines 63-68 has potential flakiness:

const start = Date.now() - 150;
// ...
expect(measureResponseTime(config, headers, false)).toBeGreaterThanOrEqual(150);

Under heavy CI load, the elapsed time between Date.now() - 150 and the measureResponseTime call could be significant, causing intermittent failures. Consider using a fixed mock for Date.now() or a wider tolerance.

Alternative approach using jest.spyOn
  it('returns elapsed ms from request-start-time for non-stream response', () => {
-   const start = Date.now() - 150;
-   const config = { headers: { 'request-start-time': start } };
-   const headers = { get: () => null };
-   expect(measureResponseTime(config, headers, false)).toBeGreaterThanOrEqual(150);
+   const now = 1000;
+   jest.spyOn(Date, 'now').mockReturnValue(now);
+   const config = { headers: { 'request-start-time': 850 } };
+   const headers = { get: () => null };
+   expect(measureResponseTime(config, headers, false)).toBe(150);
+   jest.restoreAllMocks();
  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-electron/tests/network/index.spec.js` around lines 62 - 87,
The test "returns elapsed ms from request-start-time for non-stream response" is
flaky because it uses the real Date.now(); mock Date.now() (e.g., with
jest.spyOn(Date, 'now').mockReturnValue(fixedNow)) so you can compute const
start = Date.now() - 150 deterministically before calling measureResponseTime,
then restore the spy after the test; alternatively increase the tolerance check,
but the preferred fix is to mock Date.now() around the test that uses
measureResponseTime to ensure stable timing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/bruno-electron/src/ipc/network/index.js`:
- Around line 1626-1627: The collection/folder runner reads request-duration
directly from headers (response.headers.get('request-duration')) which reflects
TTFB and bypasses measureResponseTime; update the run-collection-folder handler
to call the same measureResponseTime routine used by the standalone path (the
function named measureResponseTime) to compute total duration, set
response.responseTime to that computed value, then remove the request-duration
header; apply the same change in the error handling path (the block currently at
the later error path) so both success and error branches use measureResponseTime
rather than the raw header.

---

Nitpick comments:
In `@packages/bruno-electron/tests/network/index.spec.js`:
- Around line 62-87: The test "returns elapsed ms from request-start-time for
non-stream response" is flaky because it uses the real Date.now(); mock
Date.now() (e.g., with jest.spyOn(Date, 'now').mockReturnValue(fixedNow)) so you
can compute const start = Date.now() - 150 deterministically before calling
measureResponseTime, then restore the spy after the test; alternatively increase
the tolerance check, but the preferred fix is to mock Date.now() around the test
that uses measureResponseTime to ensure stable timing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 10e62954-628c-42bf-9dd7-f8fea7194826

📥 Commits

Reviewing files that changed from the base of the PR and between 53aa9ed and 87a41fc.

📒 Files selected for processing (2)
  • packages/bruno-electron/src/ipc/network/index.js
  • packages/bruno-electron/tests/network/index.spec.js

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 `@packages/bruno-electron/tests/network/index.spec.js`:
- Around line 63-70: The test enabling fake timers around measureResponseTime
should always restore real timers even if an assertion or helper throws; wrap
the setup/useRealTimers sequence in a try/finally so jest.useRealTimers() is
called in the finally block. Locate the test that calls jest.useFakeTimers(),
jest.setSystemTime(...), and measureResponseTime(config, headers, false) and
change it so jest.useRealTimers() is executed inside a finally, preserving the
existing config and headers variables while ensuring timers are restored.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9f609450-20f7-43ee-9694-2e1835fdcd87

📥 Commits

Reviewing files that changed from the base of the PR and between 87a41fc and f9a4617.

📒 Files selected for processing (2)
  • packages/bruno-electron/src/ipc/network/index.js
  • packages/bruno-electron/tests/network/index.spec.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/bruno-electron/src/ipc/network/index.js

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/bruno-electron/tests/network/index.spec.js (1)

26-60: Minor style inconsistency: emit('data') vs push().

Lines 30-31 use stream.emit('data', ...) while lines 40 and 49 use stream.push(...). Both work (push triggers 'data' in flowing mode), but mixing them is inconsistent. Consider standardizing on one approach for clarity.

♻️ Option: standardize on emit() throughout
 it('resolves immediately on first data chunk when closeOnFirst is true', async () => {
   const stream = new Readable({ read() {} });
   const resultPromise = promisifyStream(stream, null, true);
-  stream.push(Buffer.from('first'));
+  stream.emit('data', Buffer.from('first'));
   const result = await resultPromise;
   expect(Buffer.from(result).toString()).toBe('first');
 });

 it('calls abortController.abort() when closeOnFirst is true', async () => {
   const stream = new Readable({ read() {} });
   const abortController = { abort: jest.fn() };
   const resultPromise = promisifyStream(stream, abortController, true);
-  stream.push(Buffer.from('data'));
+  stream.emit('data', Buffer.from('data'));
   await resultPromise;
   expect(abortController.abort).toHaveBeenCalled();
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-electron/tests/network/index.spec.js` around lines 26 - 60,
Tests for promisifyStream mix stream.emit('data', ...) and stream.push(...),
causing a style inconsistency; pick one approach and make all tests consistent
(e.g., replace the emit('data', Buffer.from(...)) calls in the "resolves with
concatenated chunks on close" test with stream.push(Buffer.from(...)) so every
test uses Readable.prototype.push to deliver data), keeping the rest of the
assertions and abortController usage unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/bruno-electron/tests/network/index.spec.js`:
- Around line 26-60: Tests for promisifyStream mix stream.emit('data', ...) and
stream.push(...), causing a style inconsistency; pick one approach and make all
tests consistent (e.g., replace the emit('data', Buffer.from(...)) calls in the
"resolves with concatenated chunks on close" test with
stream.push(Buffer.from(...)) so every test uses Readable.prototype.push to
deliver data), keeping the rest of the assertions and abortController usage
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8692283c-9580-4f85-b9d1-4614c58232b4

📥 Commits

Reviewing files that changed from the base of the PR and between f9a4617 and 55f77a7.

📒 Files selected for processing (1)
  • packages/bruno-electron/tests/network/index.spec.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Response time in Bruno reflects TTFB, not total request duration (misleading vs Postman)

1 participant