Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

inspector: add initial support for network inspection #53593

Merged
merged 17 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,17 @@ added:

Enable experimental support for the `https:` protocol in `import` specifiers.

### `--experimental-network-inspection`

<!-- YAML
added:
- REPLACEME
-->

> Stability: 1 - Experimental

Enable experimental support for the network inspection with Chrome DevTools.

### `--experimental-permission`

<!-- YAML
Expand Down
69 changes: 69 additions & 0 deletions doc/api/inspector.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,75 @@ Blocks until a client (existing or connected later) has sent

An exception will be thrown if there is no active inspector.

## Node.js-specific protocol events

Node.js extends the Chrome DevTools protocol with Node.js-specific protocol events. DevTools
frontends connected to a running Node.js instance can capture these events and display them
accordingly to facilitate debugging.
The following methods broadcast a Node.js-specific protocol event to all connected frontends.
The `params` passed to the methods can be optional, depending on the protocol.

```js
// The `NodeNetwork.requestWillBeSent` event will be fired.
inspector.NodeNetwork.requestWillBeSent({
requestId: 'request-id-1',
timestamp: Date.now() / 1000,
wallTime: Date.now(),
request: {
url: 'https://nodejs.org/en',
method: 'GET',
}
});
```

### `inspector.NodeNetwork.requestWillBeSent([params])`

<!-- YAML
added:
- REPLACEME
-->

> Stability: 1 - Experimental

* `params` {Object}

This feature is only available with the `--experimental-network-inspection` flag enabled.

Broadcasts the `NodeNetwork.requestWillBeSent` event to connected frontends. This event indicates that
the application is about to send an HTTP request.

### `inspector.NodeNetwork.responseReceived([params])`

<!-- YAML
added:
- REPLACEME
-->

> Stability: 1 - Experimental

* `params` {Object}

This feature is only available with the `--experimental-network-inspection` flag enabled.

Broadcasts the `NodeNetwork.responseReceived` event to connected frontends. This event indicates that
HTTP response is available.

### `inspector.NodeNetwork.loadingFinished([params])`

<!-- YAML
added:
- REPLACEME
-->

> Stability: 1 - Experimental

* `params` {Object}

This feature is only available with the `--experimental-network-inspection` flag enabled.

Broadcasts the `NodeNetwork.loadingFinished` event to connected frontends. This event indicates that
HTTP request has finished loading.

## Support of breakpoints

The Chrome DevTools Protocol [`Debugger` domain][] allows an
Expand Down
16 changes: 16 additions & 0 deletions lib/inspector.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const {
isEnabled,
waitForDebugger,
console,
emitProtocolEvent,
} = internalBinding('inspector');

class Session extends EventEmitter {
Expand Down Expand Up @@ -188,11 +189,26 @@ function inspectorWaitForDebugger() {
throw new ERR_INSPECTOR_NOT_ACTIVE();
}

function broadcastToFrontend(eventName, params) {
validateString(eventName, 'eventName');
if (params) {
validateObject(params, 'params');
}
emitProtocolEvent(eventName, JSONStringify(params ?? {}));
}

const NodeNetwork = {
requestWillBeSent: (params) => broadcastToFrontend('NodeNetwork.requestWillBeSent', params),
responseReceived: (params) => broadcastToFrontend('NodeNetwork.responseReceived', params),
loadingFinished: (params) => broadcastToFrontend('NodeNetwork.loadingFinished', params),
};

module.exports = {
open: inspectorOpen,
close: _debugEnd,
url,
waitForDebugger: inspectorWaitForDebugger,
console,
Session,
NodeNetwork,
};
63 changes: 63 additions & 0 deletions lib/internal/inspector_network_tracking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use strict';

const {
DateNow,
} = primordials;

let dc;
let NodeNetwork;

let requestId = 0;
const getNextRequestId = () => `node-network-event-${++requestId}`;

function onClientRequestStart({ request }) {
const url = `${request.protocol}//${request.host}${request.path}`;
const wallTime = DateNow();
const timestamp = wallTime / 1000;
request._inspectorRequestId = getNextRequestId();
NodeNetwork.requestWillBeSent({

Choose a reason for hiding this comment

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

I tried it out today when I had free time. Is it possible to retrieve the original call location from the stack obtained in this way, which indicates diagnostics_channel?

Copy link
Member Author

Choose a reason for hiding this comment

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

Does 'the original call location' refer to the location in the user script where the network activity started? I mean, are you looking to retrieve the location indicated below?

const res = await fetch("https://nodejs.org/en"); // <-- here

If so, I think it's difficult with the current diagnostics_channel API. cc @nodejs/diagnostics

Choose a reason for hiding this comment

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

Yes, I mean it's not easy to do with dc.

requestId: request._inspectorRequestId,
timestamp,
wallTime,
request: {
url,
method: request.method,
},
});
}

function onClientResponseFinish({ request }) {
if (typeof request._inspectorRequestId !== 'string') {
return;
}
const timestamp = DateNow() / 1000;
NodeNetwork.responseReceived({
requestId: request._inspectorRequestId,
timestamp,
});
NodeNetwork.loadingFinished({
requestId: request._inspectorRequestId,
timestamp,
});
}

function enable() {
if (!dc) {
dc = require('diagnostics_channel');
}
if (!NodeNetwork) {
NodeNetwork = require('inspector').NodeNetwork;
}
dc.subscribe('http.client.request.start', onClientRequestStart);
dc.subscribe('http.client.response.finish', onClientResponseFinish);
}

function disable() {
dc.unsubscribe('http.client.request.start', onClientRequestStart);
dc.unsubscribe('http.client.response.finish', onClientResponseFinish);
}

module.exports = {
enable,
disable,
};
11 changes: 11 additions & 0 deletions lib/internal/process/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ function prepareExecution(options) {
const mainEntry = patchProcessObject(expandArgv1);
setupTraceCategoryState();
setupInspectorHooks();
setupNetworkInspection();
setupNavigator();
setupWarningHandler();
setupWebStorage();
Expand Down Expand Up @@ -438,6 +439,16 @@ function setupInspectorHooks() {
}
}

function setupNetworkInspection() {
if (internalBinding('config').hasInspector && getOptionValue('--experimental-network-inspection')) {
const {
enable,
disable,
} = require('internal/inspector_network_tracking');
internalBinding('inspector').setupNetworkTracking(enable, disable);
}
}

// In general deprecations are initialized wherever the APIs are implemented,
// this is used to deprecate APIs implemented in C++ where the deprecation
// utilities are not easily accessible.
Expand Down
2 changes: 2 additions & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,9 @@
V(immediate_callback_function, v8::Function) \
V(inspector_console_extension_installer, v8::Function) \
V(inspector_disable_async_hooks, v8::Function) \
V(inspector_disable_network_tracking, v8::Function) \
V(inspector_enable_async_hooks, v8::Function) \
V(inspector_enable_network_tracking, v8::Function) \
V(maybe_cache_generated_source_map, v8::Function) \
V(messaging_deserialize_create_object, v8::Function) \
V(message_port, v8::Object) \
Expand Down
84 changes: 84 additions & 0 deletions src/inspector/network_agent.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#include "network_agent.h"
#include "network_inspector.h"

namespace node {
namespace inspector {
namespace protocol {

std::unique_ptr<Network::Request> Request(const String& url,
const String& method) {
return Network::Request::create().setUrl(url).setMethod(method).build();
}

NetworkAgent::NetworkAgent(NetworkInspector* inspector)
: inspector_(inspector) {
event_notifier_map_["requestWillBeSent"] = &NetworkAgent::requestWillBeSent;
event_notifier_map_["responseReceived"] = &NetworkAgent::responseReceived;
event_notifier_map_["loadingFinished"] = &NetworkAgent::loadingFinished;
}

void NetworkAgent::emitNotification(
const String& event, std::unique_ptr<protocol::DictionaryValue> params) {
if (!inspector_->IsEnabled()) return;
auto it = event_notifier_map_.find(event);
if (it != event_notifier_map_.end()) {
(this->*(it->second))(std::move(params));
}
}

void NetworkAgent::Wire(UberDispatcher* dispatcher) {
frontend_ = std::make_unique<Network::Frontend>(dispatcher->channel());
Network::Dispatcher::wire(dispatcher, this);
}

DispatchResponse NetworkAgent::enable() {
inspector_->Enable();
return DispatchResponse::OK();
}

DispatchResponse NetworkAgent::disable() {
inspector_->Disable();
return DispatchResponse::OK();
}

void NetworkAgent::requestWillBeSent(
std::unique_ptr<protocol::DictionaryValue> params) {
String request_id;
params->getString("requestId", &request_id);
double timestamp;
params->getDouble("timestamp", &timestamp);
double wall_time;
params->getDouble("wallTime", &wall_time);
auto request = params->getObject("request");
String url;
request->getString("url", &url);
String method;
request->getString("method", &method);

frontend_->requestWillBeSent(
request_id, Request(url, method), timestamp, wall_time);
}

void NetworkAgent::responseReceived(
std::unique_ptr<protocol::DictionaryValue> params) {
String request_id;
params->getString("requestId", &request_id);
double timestamp;
params->getDouble("timestamp", &timestamp);

frontend_->responseReceived(request_id, timestamp);
}

void NetworkAgent::loadingFinished(
std::unique_ptr<protocol::DictionaryValue> params) {
String request_id;
params->getString("requestId", &request_id);
double timestamp;
params->getDouble("timestamp", &timestamp);

frontend_->loadingFinished(request_id, timestamp);
}

} // namespace protocol
} // namespace inspector
} // namespace node
49 changes: 49 additions & 0 deletions src/inspector/network_agent.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#ifndef SRC_INSPECTOR_NETWORK_AGENT_H_
#define SRC_INSPECTOR_NETWORK_AGENT_H_

#include "node/inspector/protocol/Network.h"

#include <unordered_map>

namespace node {

namespace inspector {
class NetworkInspector;

namespace protocol {

std::unique_ptr<Network::Request> Request(const String& url,
const String& method);

class NetworkAgent : public Network::Backend {
public:
explicit NetworkAgent(NetworkInspector* inspector);

void Wire(UberDispatcher* dispatcher);

DispatchResponse enable() override;

DispatchResponse disable() override;

void emitNotification(const String& event,
std::unique_ptr<protocol::DictionaryValue> params);

void requestWillBeSent(std::unique_ptr<protocol::DictionaryValue> params);

void responseReceived(std::unique_ptr<protocol::DictionaryValue> params);

void loadingFinished(std::unique_ptr<protocol::DictionaryValue> params);

private:
NetworkInspector* inspector_;
std::shared_ptr<Network::Frontend> frontend_;
using EventNotifier =
void (NetworkAgent::*)(std::unique_ptr<protocol::DictionaryValue>);
std::unordered_map<String, EventNotifier> event_notifier_map_;
};

} // namespace protocol
} // namespace inspector
} // namespace node

#endif // SRC_INSPECTOR_NETWORK_AGENT_H_
Loading
Loading