Skip to content

Commit

Permalink
inspector: add initial support for network inspection
Browse files Browse the repository at this point in the history
PR-URL: #53593
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
  • Loading branch information
cola119 authored and targos committed Jul 28, 2024
1 parent a94c3ae commit de1fbc2
Show file tree
Hide file tree
Showing 20 changed files with 789 additions and 3 deletions.
11 changes: 11 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,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.

## Integration with DevTools

The `node:inspector` module provides an API for integrating with devtools that support Chrome DevTools Protocol.
DevTools frontends connected to a running Node.js instance can capture protocol events emitted from the instance
and display them accordingly to facilitate debugging.
The following methods broadcast a protocol event to all connected frontends.
The `params` passed to the methods can be optional, depending on the protocol.

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

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

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

> Stability: 1 - Experimental
* `params` {Object}

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

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

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

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

> Stability: 1 - Experimental
* `params` {Object}

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

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

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

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

> Stability: 1 - Experimental
* `params` {Object}

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

Broadcasts the `Network.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 Network = {
requestWillBeSent: (params) => broadcastToFrontend('Network.requestWillBeSent', params),
responseReceived: (params) => broadcastToFrontend('Network.responseReceived', params),
loadingFinished: (params) => broadcastToFrontend('Network.loadingFinished', params),
};

module.exports = {
open: inspectorOpen,
close: _debugEnd,
url,
waitForDebugger: inspectorWaitForDebugger,
console,
Session,
Network,
};
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 Network;

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();
Network.requestWillBeSent({
requestId: request._inspectorRequestId,
timestamp,
wallTime,
request: {
url,
method: request.method,
},
});
}

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

function enable() {
if (!dc) {
dc = require('diagnostics_channel');
}
if (!Network) {
Network = require('inspector').Network;
}
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 @@ -106,6 +106,7 @@ function prepareExecution(options) {
const mainEntry = patchProcessObject(expandArgv1);
setupTraceCategoryState();
setupInspectorHooks();
setupNetworkInspection();
setupNavigator();
setupWarningHandler();
setupUndici();
Expand Down Expand Up @@ -513,6 +514,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 @@ -448,7 +448,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

0 comments on commit de1fbc2

Please sign in to comment.