Skip to content

Commit

Permalink
inspector: add undici http tracking support
Browse files Browse the repository at this point in the history
Add basic undici http tracking support via inspector protocol. This
allows tracking `fetch` calls with an inspector.

PR-URL: #56488
Refs: #53946
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Benjamin Gruenbaum <[email protected]>
Reviewed-By: Ethan Arrowood <[email protected]>
Reviewed-By: Matteo Collina <[email protected]>
  • Loading branch information
legendecas authored Jan 8, 2025
1 parent 7b472fd commit 4f45ace
Show file tree
Hide file tree
Showing 6 changed files with 367 additions and 8 deletions.
23 changes: 23 additions & 0 deletions lib/internal/inspector/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,28 @@ const {
const { now } = require('internal/perf/utils');
const kInspectorRequestId = Symbol('kInspectorRequestId');

// https://chromedevtools.github.io/devtools-protocol/1-3/Network/#type-ResourceType
const kResourceType = {
Document: 'Document',
Stylesheet: 'Stylesheet',
Image: 'Image',
Media: 'Media',
Font: 'Font',
Script: 'Script',
TextTrack: 'TextTrack',
XHR: 'XHR',
Fetch: 'Fetch',
Prefetch: 'Prefetch',
EventSource: 'EventSource',
WebSocket: 'WebSocket',
Manifest: 'Manifest',
SignedExchange: 'SignedExchange',
Ping: 'Ping',
CSPViolationReport: 'CSPViolationReport',
Preflight: 'Preflight',
Other: 'Other',
};

/**
* Return a monotonically increasing time in seconds since an arbitrary point in the past.
* @returns {number}
Expand All @@ -26,6 +48,7 @@ function getNextRequestId() {

module.exports = {
kInspectorRequestId,
kResourceType,
getMonotonicTime,
getNextRequestId,
};
6 changes: 3 additions & 3 deletions lib/internal/inspector/network_http.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ const {

const {
kInspectorRequestId,
kResourceType,
getMonotonicTime,
getNextRequestId,
} = require('internal/inspector/network');
const dc = require('diagnostics_channel');
const { Network } = require('inspector');

const kResourceType = 'Other';
const kRequestUrl = Symbol('kRequestUrl');

// Convert a Headers object (Map<string, number | string | string[]>) to a plain object (Map<string, string>)
Expand Down Expand Up @@ -79,7 +79,7 @@ function onClientRequestError({ request, error }) {
Network.loadingFailed({
requestId: request[kInspectorRequestId],
timestamp: getMonotonicTime(),
type: kResourceType,
type: kResourceType.Other,
errorText: error.message,
});
}
Expand All @@ -96,7 +96,7 @@ function onClientResponseFinish({ request, response }) {
Network.responseReceived({
requestId: request[kInspectorRequestId],
timestamp: getMonotonicTime(),
type: kResourceType,
type: kResourceType.Other,
response: {
url: request[kRequestUrl],
status: response.statusCode,
Expand Down
141 changes: 141 additions & 0 deletions lib/internal/inspector/network_undici.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
'use strict';

const {
DateNow,
} = primordials;

const {
kInspectorRequestId,
kResourceType,
getMonotonicTime,
getNextRequestId,
} = require('internal/inspector/network');
const dc = require('diagnostics_channel');
const { Network } = require('inspector');

// Convert an undici request headers array to a plain object (Map<string, string>)
function requestHeadersArrayToDictionary(headers) {
const dict = {};
for (let idx = 0; idx < headers.length; idx += 2) {
const key = `${headers[idx]}`;
const value = `${headers[idx + 1]}`;
dict[key] = value;
}
return dict;
};

// Convert an undici response headers array to a plain object (Map<string, string>)
function responseHeadersArrayToDictionary(headers) {
const dict = {};
for (let idx = 0; idx < headers.length; idx += 2) {
const key = `${headers[idx]}`;
const value = `${headers[idx + 1]}`;
const prevValue = dict[key];

if (typeof prevValue === 'string') {
// ChromeDevTools frontend treats 'set-cookie' as a special case
// https://github.com/ChromeDevTools/devtools-frontend/blob/4275917f84266ef40613db3c1784a25f902ea74e/front_end/core/sdk/NetworkRequest.ts#L1368
if (key.toLowerCase() === 'set-cookie') dict[key] = `${prevValue}\n${value}`;
else dict[key] = `${prevValue}, ${value}`;
} else {
dict[key] = value;
}
}
return dict;
};

/**
* When a client request starts, emit Network.requestWillBeSent event.
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-requestWillBeSent
* @param {{ request: undici.Request }} event
*/
function onClientRequestStart({ request }) {
const url = `${request.origin}${request.path}`;
request[kInspectorRequestId] = getNextRequestId();
Network.requestWillBeSent({
requestId: request[kInspectorRequestId],
timestamp: getMonotonicTime(),
wallTime: DateNow(),
request: {
url,
method: request.method,
headers: requestHeadersArrayToDictionary(request.headers),
},
});
}

/**
* When a client request errors, emit Network.loadingFailed event.
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-loadingFailed
* @param {{ request: undici.Request, error: any }} event
*/
function onClientRequestError({ request, error }) {
if (typeof request[kInspectorRequestId] !== 'string') {
return;
}
Network.loadingFailed({
requestId: request[kInspectorRequestId],
timestamp: getMonotonicTime(),
// TODO(legendecas): distinguish between `undici.request` and `undici.fetch`.
type: kResourceType.Fetch,
errorText: error.message,
});
}

/**
* When response headers are received, emit Network.responseReceived event.
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-responseReceived
* @param {{ request: undici.Request, response: undici.Response }} event
*/
function onClientResponseHeaders({ request, response }) {
if (typeof request[kInspectorRequestId] !== 'string') {
return;
}
const url = `${request.origin}${request.path}`;
Network.responseReceived({
requestId: request[kInspectorRequestId],
timestamp: getMonotonicTime(),
// TODO(legendecas): distinguish between `undici.request` and `undici.fetch`.
type: kResourceType.Fetch,
response: {
url,
status: response.statusCode,
statusText: response.statusText,
headers: responseHeadersArrayToDictionary(response.headers),
},
});
}

/**
* When a response is completed, emit Network.loadingFinished event.
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-loadingFinished
* @param {{ request: undici.Request, response: undici.Response }} event
*/
function onClientResponseFinish({ request }) {
if (typeof request[kInspectorRequestId] !== 'string') {
return;
}
Network.loadingFinished({
requestId: request[kInspectorRequestId],
timestamp: getMonotonicTime(),
});
}

function enable() {
dc.subscribe('undici:request:create', onClientRequestStart);
dc.subscribe('undici:request:error', onClientRequestError);
dc.subscribe('undici:request:headers', onClientResponseHeaders);
dc.subscribe('undici:request:trailers', onClientResponseFinish);
}

function disable() {
dc.subscribe('undici:request:create', onClientRequestStart);
dc.subscribe('undici:request:error', onClientRequestError);
dc.subscribe('undici:request:headers', onClientResponseHeaders);
dc.subscribe('undici:request:trailers', onClientResponseFinish);
}

module.exports = {
enable,
disable,
};
6 changes: 2 additions & 4 deletions lib/internal/inspector_network_tracking.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@

function enable() {
require('internal/inspector/network_http').enable();
// TODO: add undici request/websocket tracking.
// https://github.com/nodejs/node/issues/53946
require('internal/inspector/network_undici').enable();
}

function disable() {
require('internal/inspector/network_http').disable();
// TODO: add undici request/websocket tracking.
// https://github.com/nodejs/node/issues/53946
require('internal/inspector/network_undici').disable();
}

module.exports = {
Expand Down
3 changes: 2 additions & 1 deletion src/node_builtins.cc
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const {
#if !HAVE_INSPECTOR
"inspector", "inspector/promises", "internal/util/inspector",
"internal/inspector/network", "internal/inspector/network_http",
"internal/inspector_async_hook", "internal/inspector_network_tracking",
"internal/inspector/network_undici", "internal/inspector_async_hook",
"internal/inspector_network_tracking",
#endif // !HAVE_INSPECTOR

#if !NODE_USE_V8_PLATFORM || !defined(NODE_HAVE_I18N_SUPPORT)
Expand Down
Loading

0 comments on commit 4f45ace

Please sign in to comment.