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 all 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.

## 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 @@ -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