Skip to content

feat(fetch): expose timings#32647

Closed
Skn0tt wants to merge 5 commits into
microsoft:mainfrom
Skn0tt:expose-apiresponse-timing
Closed

feat(fetch): expose timings#32647
Skn0tt wants to merge 5 commits into
microsoft:mainfrom
Skn0tt:expose-apiresponse-timing

Conversation

@Skn0tt

@Skn0tt Skn0tt commented Sep 17, 2024

Copy link
Copy Markdown
Member

Closes #19621. Adds the same timings() we have for browser responses to APIResponse. Please apply some extra care in reviewing the timing calculations.

In the protocol change, I was unsure wether to extend the existing ResourceTiming type or to add another property. I went with the added property to be in-line with how the events work for browser requests - let me know if we should do it differently instead.

@Skn0tt Skn0tt self-assigned this Sep 17, 2024
@github-actions

This comment has been minimized.

@Skn0tt Skn0tt force-pushed the expose-apiresponse-timing branch from 3de1612 to a7ea946 Compare September 17, 2024 13:56
@github-actions

This comment has been minimized.

@Skn0tt Skn0tt force-pushed the expose-apiresponse-timing branch from 9c743d9 to 0eb69c3 Compare September 17, 2024 14:14
@github-actions

This comment has been minimized.

@Skn0tt Skn0tt requested a review from dgozman September 17, 2024 14:19
make responseEnd optional

make it more similar to existing types

missed one!
@Skn0tt Skn0tt force-pushed the expose-apiresponse-timing branch from 0eb69c3 to 8de8383 Compare September 17, 2024 14:24
@Skn0tt Skn0tt marked this pull request as ready for review September 17, 2024 14:24
- `domainLookupStart` <[float]> Time immediately before the browser starts the domain name lookup for the
resource. The value is given in milliseconds relative to `startTime`, -1 if not available.
- `domainLookupEnd` <[float]> Time immediately after the browser starts the domain name lookup for the resource.
- `domainLookupEnd` <[float]> Time immediately after the browser ends the domain name lookup for the resource.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this is a drive-by fix - pretty sure it shouldn't say "start the domain name lookup"

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

socket.on('secureConnect', () => { tlsHandshakeAt = monotonicTime(); });

// socks / http proxy
socket.on('proxyConnect', () => { tcpConnectionAt = monotonicTime(); });

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Adding timings to the protocol uncovered that tcpConnectionAt was undefined for requests over SOCKS and HTTPS Proxy. Turns out that the library we use for that doesn't emit the connect event, but the proxyConnect event instead.

@github-actions

This comment has been minimized.

const endAt = monotonicTime();
// spec: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming
const timing: channels.ResourceTiming = {
startTime: startAt,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The docs say startTime is a wall time, not monotonic time.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

done in b717257

// spec: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming
const timing: channels.ResourceTiming = {
startTime: startAt,
domainLookupStart: dnsLookupAt ? 0 : -1,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why zero and not relativeTime()?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

My understand is that the browser has some other steps like cache resolution before the DNS lookup, so there might be time between startTime and the DNS lookup start. On Node.js, I don't think there's anything between that - so it's zero, because we know the DNS lookup happens immediately after the request start.

const timing: channels.ResourceTiming = {
startTime: startAt,
domainLookupStart: dnsLookupAt ? 0 : -1,
domainLookupEnd: dnsLookupAt ? dnsLookupAt! - startAt : -1,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

relativeTime()?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

done in b1d523b

body
body,
timing,
responseEndTiming,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why do we have two sets of timings now?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

For browser requests, most of the timings are transmitted in the Response type, and then responseEndTiming arrives later in the requestFinished and requestFailed events. I opted to make this similar, so we have a big set of timings in timing and the final timing in responseEndTiming.

I thought about amending the ResourceTiming type instead, but then requestFinished would suddenly have the response end time both in responseEndTiming and in response.timing.responseEnd - that felt confusing.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

Test results for "tests 1"

2 failed
❌ [playwright-test] › babel.spec.ts:135:5 › should not transform external
❌ [playwright-test] › fixture-errors.spec.ts:471:5 › should not give enough time for second fixture teardown after timeout

3 flaky ⚠️ [firefox-library] › library/inspector/cli-codegen-2.spec.ts:407:7 › cli codegen › click should emit events in order
⚠️ [playwright-test] › ui-mode-test-ct.spec.ts:59:5 › should run component tests after editing test
⚠️ [webkit-library] › library/download.spec.ts:698:3 › should convert navigation to a resource with unsupported mime type into download

35496 passed, 659 skipped
✔️✔️✔️

Merge workflow run.

@Skn0tt

Skn0tt commented Sep 27, 2024

Copy link
Copy Markdown
Member Author

We discussed this with the team and decided against exposing this via the API. Playwright isn't a network performance testing tool, and we don't want people using it like one. Rough timings can easily be measured in userland.

I'll open a separate PR to fix the bugs around HTTP / SOCKS Proxy we found in the existing measurements for HAR timings.

@Skn0tt Skn0tt closed this Sep 27, 2024
Skn0tt added a commit that referenced this pull request Oct 7, 2024
)

Fixes a bug discovered in
#32647. When using http
proxy, the `connect` event isn't emitted so we don't populate
`tcpConnectionAt`. The updated version of `https-proxy-agent` emits a
`proxyConnect` as a replacement, so this PR updates and listens to that
event.
For socks proxies, the `on("socket")` event is emitted once the SOCKS
connection is established, which is the equivalent of having a TCP
connection available.

---------

Signed-off-by: Simon Knott <info@simonknott.de>
Co-authored-by: Max Schmitt <max@schmitt.mx>
shahzad31 added a commit to elastic/synthetics that referenced this pull request May 19, 2026
Playwright's APIResponse doesn't expose securityDetails(), serverAddr(),
or request/response timings (upstream microsoft/playwright#32647 and
microsoft/playwright#34938 were both declined; see microsoft/playwright#40905
for the current ask).

To still surface this for HTTPS API monitoring use cases (cert expiry,
remote-address alerting, response size tracking), open a side-channel
tls.connect() in parallel with each request and fold the result into
the NetworkInfo entry:

- securityDetails: issuer, subjectName, protocol, validFrom, validTo
- remoteIPAddress / remotePort
- coarse dns / connect / ssl timings

Probes are cached per host:port within a journey and silently skipped
for HTTP or on failure (timeout, refused, untrusted) so they never
affect the actual request.

Also derive request and response body bytes (Content-Length preferred,
buffer fallback) and emit server.ip / server.port in the JSON reporter.

Co-authored-by: Cursor <cursoragent@cursor.com>
shahzad31 added a commit to elastic/synthetics that referenced this pull request Jun 15, 2026
Playwright's APIResponse doesn't expose securityDetails(), serverAddr(),
or request/response timings (upstream microsoft/playwright#32647 and
microsoft/playwright#34938 were both declined; see microsoft/playwright#40905
for the current ask).

To still surface this for HTTPS API monitoring use cases (cert expiry,
remote-address alerting, response size tracking), open a side-channel
tls.connect() in parallel with each request and fold the result into
the NetworkInfo entry:

- securityDetails: issuer, subjectName, protocol, validFrom, validTo
- remoteIPAddress / remotePort
- coarse dns / connect / ssl timings

Probes are cached per host:port within a journey and silently skipped
for HTTP or on failure (timeout, refused, untrusted) so they never
affect the actual request.

Also derive request and response body bytes (Content-Length preferred,
buffer fallback) and emit server.ip / server.port in the JSON reporter.

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

[Question] Is there a way to return an api request response time?

2 participants