inspector: add Network.Initiator in inspector protocol

Add initiator stack trace in inspector network events, reflecting
the location where the script created the request.

The `http.client.request.created` event is closer to where user code
creates the http request, and correctly reflects which script
initiated the request.

PR-URL: https://github.com/nodejs/node/pull/56805
Refs: https://github.com/nodejs/node/issues/53946
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Kohei Ueno <kohei.ueno119@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
This commit is contained in:
Chengzhong Wu 2025-02-05 18:03:57 +00:00 committed by Node.js GitHub Bot
parent 297a4dd339
commit b18153598b
12 changed files with 92 additions and 15 deletions

View File

@ -44,11 +44,11 @@ const convertHeaderObject = (headers = {}) => {
};
/**
* When a client request starts, emit Network.requestWillBeSent event.
* When a client request is created, emit Network.requestWillBeSent event.
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-requestWillBeSent
* @param {{ request: import('http').ClientRequest }} event
*/
function onClientRequestStart({ request }) {
function onClientRequestCreated({ request }) {
request[kInspectorRequestId] = getNextRequestId();
const { 0: host, 1: headers } = convertHeaderObject(request.getHeaders());
@ -115,13 +115,13 @@ function onClientResponseFinish({ request, response }) {
}
function enable() {
dc.subscribe('http.client.request.start', onClientRequestStart);
dc.subscribe('http.client.request.created', onClientRequestCreated);
dc.subscribe('http.client.request.error', onClientRequestError);
dc.subscribe('http.client.response.finish', onClientResponseFinish);
}
function disable() {
dc.unsubscribe('http.client.request.start', onClientRequestStart);
dc.unsubscribe('http.client.request.created', onClientRequestCreated);
dc.unsubscribe('http.client.request.error', onClientRequestError);
dc.unsubscribe('http.client.response.finish', onClientResponseFinish);
}

View File

@ -129,10 +129,10 @@ function enable() {
}
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);
dc.unsubscribe('undici:request:create', onClientRequestStart);
dc.unsubscribe('undici:request:error', onClientRequestError);
dc.unsubscribe('undici:request:headers', onClientResponseHeaders);
dc.unsubscribe('undici:request:trailers', onClientResponseFinish);
}
module.exports = {

View File

@ -29,8 +29,9 @@ std::unique_ptr<Network::Response> createResponse(
.build();
}
NetworkAgent::NetworkAgent(NetworkInspector* inspector)
: inspector_(inspector) {
NetworkAgent::NetworkAgent(NetworkInspector* inspector,
v8_inspector::V8Inspector* v8_inspector)
: inspector_(inspector), v8_inspector_(v8_inspector) {
event_notifier_map_["requestWillBeSent"] = &NetworkAgent::requestWillBeSent;
event_notifier_map_["responseReceived"] = &NetworkAgent::responseReceived;
event_notifier_map_["loadingFailed"] = &NetworkAgent::loadingFailed;
@ -75,6 +76,13 @@ void NetworkAgent::requestWillBeSent(
String method;
request->getString("method", &method);
std::unique_ptr<Network::Initiator> initiator =
Network::Initiator::create()
.setType(Network::Initiator::TypeEnum::Script)
.setStack(
v8_inspector_->captureStackTrace(true)->buildInspectorObject(0))
.build();
ErrorSupport errors;
errors.Push();
errors.SetName("headers");
@ -86,6 +94,7 @@ void NetworkAgent::requestWillBeSent(
frontend_->requestWillBeSent(request_id,
createRequest(url, method, std::move(headers)),
std::move(initiator),
timestamp,
wall_time);
}

View File

@ -14,7 +14,8 @@ namespace protocol {
class NetworkAgent : public Network::Backend {
public:
explicit NetworkAgent(NetworkInspector* inspector);
explicit NetworkAgent(NetworkInspector* inspector,
v8_inspector::V8Inspector* v8_inspector);
void Wire(UberDispatcher* dispatcher);
@ -35,6 +36,7 @@ class NetworkAgent : public Network::Backend {
private:
NetworkInspector* inspector_;
v8_inspector::V8Inspector* v8_inspector_;
std::shared_ptr<Network::Frontend> frontend_;
using EventNotifier =
void (NetworkAgent::*)(std::unique_ptr<protocol::DictionaryValue>);

View File

@ -3,9 +3,10 @@
namespace node {
namespace inspector {
NetworkInspector::NetworkInspector(Environment* env)
NetworkInspector::NetworkInspector(Environment* env,
v8_inspector::V8Inspector* v8_inspector)
: enabled_(false), env_(env) {
network_agent_ = std::make_unique<protocol::NetworkAgent>(this);
network_agent_ = std::make_unique<protocol::NetworkAgent>(this, v8_inspector);
}
NetworkInspector::~NetworkInspector() {
network_agent_.reset();

View File

@ -11,7 +11,8 @@ namespace inspector {
class NetworkInspector {
public:
explicit NetworkInspector(Environment* env);
explicit NetworkInspector(Environment* env,
v8_inspector::V8Inspector* v8_inspector);
~NetworkInspector();
void Wire(protocol::UberDispatcher* dispatcher);

View File

@ -97,6 +97,7 @@
'action_name': 'node_protocol_generated_sources',
'inputs': [
'node_protocol_config.json',
'node_protocol.pdl',
'<(SHARED_INTERMEDIATE_DIR)/src/node_protocol.json',
'<@(node_protocol_files)',
'<(protocol_tool_path)/code_generator.py',

View File

@ -101,6 +101,8 @@ experimental domain NodeWorker
# Partial support for Network domain of ChromeDevTools Protocol.
# https://chromedevtools.github.io/devtools-protocol/tot/Network
experimental domain Network
depends on Runtime
# Resource type as it was perceived by the rendering engine.
type ResourceType extends string
enum
@ -132,6 +134,31 @@ experimental domain Network
# Monotonically increasing time in seconds since an arbitrary point in the past.
type MonotonicTime extends number
# Information about the request initiator.
type Initiator extends object
properties
# Type of this initiator.
enum type
parser
script
preload
SignedExchange
preflight
other
# Initiator JavaScript stack trace, set for Script only.
# Requires the Debugger domain to be enabled.
optional Runtime.StackTrace stack
# Initiator URL, set for Parser type or for Script type (when script is importing module) or for SignedExchange type.
optional string url
# Initiator line number, set for Parser type or for Script type (when script is importing
# module) (0-based).
optional number lineNumber
# Initiator column number, set for Parser type or for Script type (when script is importing
# module) (0-based).
optional number columnNumber
# Set if another request triggered this request (e.g. preflight).
optional RequestId requestId
# HTTP request data.
type Request extends object
properties
@ -163,6 +190,8 @@ experimental domain Network
RequestId requestId
# Request data.
Request request
# Request initiator.
Initiator initiator
# Timestamp.
MonotonicTime timestamp
# Timestamp.

View File

@ -5,6 +5,17 @@
"output": "node/inspector/protocol",
"namespace": ["node", "inspector", "protocol"]
},
"imported": {
"path": "../../deps/v8/include/js_protocol.pdl",
"header": "<v8-inspector-protocol.h>",
"namespace": ["v8_inspector", "protocol"],
"options": [
{
"domain": "Runtime",
"imported": ["StackTrace"]
}
]
},
"exported": {
"package": "include/inspector",
"output": "../../include/inspector",

View File

@ -241,7 +241,8 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
}
runtime_agent_ = std::make_unique<protocol::RuntimeAgent>();
runtime_agent_->Wire(node_dispatcher_.get());
network_inspector_ = std::make_unique<NetworkInspector>(env);
network_inspector_ =
std::make_unique<NetworkInspector>(env, inspector.get());
network_inspector_->Wire(node_dispatcher_.get());
}

View File

@ -65,6 +65,13 @@ const terminate = () => {
inspector.close();
};
function findFrameInInitiator(scriptName, initiator) {
const frame = initiator.stack.callFrames.find((it) => {
return it.url === scriptName;
});
return frame;
}
const testHttpGet = () => new Promise((resolve, reject) => {
session.on('Network.requestWillBeSent', common.mustCall(({ params }) => {
assert.ok(params.requestId.startsWith('node-network-event-'));
@ -77,6 +84,10 @@ const testHttpGet = () => new Promise((resolve, reject) => {
assert.strictEqual(params.request.headers['x-header1'], 'value1, value2');
assert.strictEqual(typeof params.timestamp, 'number');
assert.strictEqual(typeof params.wallTime, 'number');
assert.strictEqual(typeof params.initiator, 'object');
assert.strictEqual(params.initiator.type, 'script');
assert.ok(findFrameInInitiator(__filename, params.initiator));
}));
session.on('Network.responseReceived', common.mustCall(({ params }) => {
assert.ok(params.requestId.startsWith('node-network-event-'));

View File

@ -64,6 +64,13 @@ const terminate = () => {
inspector.close();
};
function findFrameInInitiator(scriptName, initiator) {
const frame = initiator.stack.callFrames.find((it) => {
return it.url === scriptName;
});
return frame;
}
function verifyRequestWillBeSent({ method, params }, expect) {
assert.strictEqual(method, 'Network.requestWillBeSent');
@ -78,6 +85,10 @@ function verifyRequestWillBeSent({ method, params }, expect) {
assert.strictEqual(typeof params.timestamp, 'number');
assert.strictEqual(typeof params.wallTime, 'number');
assert.strictEqual(typeof params.initiator, 'object');
assert.strictEqual(params.initiator.type, 'script');
assert.ok(findFrameInInitiator(__filename, params.initiator));
return params;
}