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 12 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
68 changes: 68 additions & 0 deletions doc/api/inspector.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,74 @@ 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
cola119 marked this conversation as resolved.
Show resolved Hide resolved

Node.js has dedicated DevTools protocols for debugging. The following methods emits a protocol
cola119 marked this conversation as resolved.
Show resolved Hide resolved
event with optional `params`. These methods allow you to send custom events that conform to the
DevTools protocol, facilitating integration with debugging and inspection tools. The emitted
events can be captured by connected DevTools Frontend instances, such as Chrome DevTools.

```js
// `NodeNetwork.requestWillBeSent` event will be fired.
cola119 marked this conversation as resolved.
Show resolved Hide resolved
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.

Emits the `NodeNetwork.requestWillBeSent` event. This event indicates that the application is
about to send an HTTP request.
cola119 marked this conversation as resolved.
Show resolved Hide resolved

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

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

> Stability: 1 - Experimental

* `params` {Object}

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

Emits the `NodeNetwork.responseReceived` event. This event indicates that HTTP response is
available.
cola119 marked this conversation as resolved.
Show resolved Hide resolved

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

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

> Stability: 1 - Experimental

* `params` {Object}

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

Emits the `NodeNetwork.loadingFinished` event. This event indicates that HTTP request has
finished loading.
cola119 marked this conversation as resolved.
Show resolved Hide resolved

## 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 emit(eventName, params) {
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
validateString(eventName, 'eventName');
if (params) {
validateObject(params, 'params');
}
emitProtocolEvent(eventName, JSONStringify(params ?? {}));
}

const NodeNetwork = {
requestWillBeSent: (params) => emit('NodeNetwork.requestWillBeSent', params),
responseReceived: (params) => emit('NodeNetwork.responseReceived', params),
loadingFinished: (params) => emit('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
88 changes: 88 additions & 0 deletions src/inspector/network_agent.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#include "network_agent.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(Environment* env) : enabled_(false), env_(env) {
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 (!enabled_) 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() {
enabled_ = true;
if (auto agent = env_->inspector_agent()) {
agent->EnableNetworkTracking();
}
return DispatchResponse::OK();
}

DispatchResponse NetworkAgent::disable() {
enabled_ = false;
if (auto agent = env_->inspector_agent()) {
agent->DisableNetworkTracking();
}
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
51 changes: 51 additions & 0 deletions src/inspector/network_agent.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#ifndef SRC_INSPECTOR_NETWORK_AGENT_H_
#define SRC_INSPECTOR_NETWORK_AGENT_H_

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

#include <unordered_map>

namespace node {
class Environment;

namespace inspector {
namespace protocol {

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

class NetworkAgent : public Network::Backend {
public:
explicit NetworkAgent(Environment* env);

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:
bool enabled_;
Environment* env_;
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_
8 changes: 8 additions & 0 deletions src/inspector/node_inspector.gypi
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
'src/inspector/tracing_agent.h',
'src/inspector/worker_agent.cc',
'src/inspector/worker_agent.h',
'src/inspector/network_agent.cc',
'src/inspector/network_agent.h',
'src/inspector/node_network_agent.cc',
'src/inspector/node_network_agent.h',
'src/inspector/worker_inspector.cc',
'src/inspector/worker_inspector.h',
],
Expand All @@ -36,6 +40,10 @@
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/NodeTracing.h',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/NodeRuntime.cpp',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/NodeRuntime.h',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Network.cpp',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Network.h',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/NodeNetwork.cpp',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/NodeNetwork.h',
],
'node_protocol_files': [
'<(protocol_tool_path)/lib/Allocator_h.template',
Expand Down
Loading
Loading