feat: WebFrameMain.collectJavaScriptCallStack() (#44937)
* feat: WebFrameMain.unresponsiveDocumentJSCallStack Co-authored-by: Samuel Maddock <smaddock@slack-corp.com> * Revert "feat: WebFrameMain.unresponsiveDocumentJSCallStack" This reverts commit e0612bc1a00a5282cba5df97da3c9c90e96ef244. Co-authored-by: Samuel Maddock <smaddock@slack-corp.com> * feat: frame.collectJavaScriptCallStack() Co-authored-by: Samuel Maddock <smaddock@slack-corp.com> * feat: frame.collectJavaScriptCallStack() Co-authored-by: Samuel Maddock <smaddock@slack-corp.com> * Update web-frame-main.md Co-authored-by: Sam Maddock <smaddock@slack-corp.com> --------- Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Samuel Maddock <smaddock@slack-corp.com>
This commit is contained in:
parent
7d2d5240bd
commit
e1d9df224c
5 changed files with 131 additions and 1 deletions
|
@ -142,6 +142,29 @@ ipcRenderer.on('port', (e, msg) => {
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `frame.collectJavaScriptCallStack()` _Experimental_
|
||||||
|
|
||||||
|
Returns `Promise<string> | Promise<void>` - A promise that resolves with the currently running JavaScript call
|
||||||
|
stack. If no JavaScript runs in the frame, the promise will never resolve. In cases where the call stack is
|
||||||
|
otherwise unable to be collected, it will return `undefined`.
|
||||||
|
|
||||||
|
This can be useful to determine why the frame is unresponsive in cases where there's long-running JavaScript.
|
||||||
|
For more information, see the [proposed Crash Reporting API.](https://wicg.github.io/crash-reporting/)
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { app } = require('electron')
|
||||||
|
|
||||||
|
app.commandLine.appendSwitch('enable-features', 'DocumentPolicyIncludeJSCallStacksInCrashReports')
|
||||||
|
|
||||||
|
app.on('web-contents-created', (_, webContents) => {
|
||||||
|
webContents.on('unresponsive', async () => {
|
||||||
|
// Interrupt execution and collect call stack from unresponsive renderer
|
||||||
|
const callStack = await webContents.mainFrame.collectJavaScriptCallStack()
|
||||||
|
console.log('Renderer unresponsive\n', callStack)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
### Instance Properties
|
### Instance Properties
|
||||||
|
|
||||||
#### `frame.ipc` _Readonly_
|
#### `frame.ipc` _Readonly_
|
||||||
|
|
|
@ -9,9 +9,11 @@
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "base/feature_list.h"
|
||||||
#include "base/logging.h"
|
#include "base/logging.h"
|
||||||
#include "base/no_destructor.h"
|
#include "base/no_destructor.h"
|
||||||
#include "content/browser/renderer_host/render_frame_host_impl.h" // nogncheck
|
#include "content/browser/renderer_host/render_frame_host_impl.h" // nogncheck
|
||||||
|
#include "content/browser/renderer_host/render_process_host_impl.h" // nogncheck
|
||||||
#include "content/public/browser/frame_tree_node_id.h"
|
#include "content/public/browser/frame_tree_node_id.h"
|
||||||
#include "content/public/browser/render_frame_host.h"
|
#include "content/public/browser/render_frame_host.h"
|
||||||
#include "content/public/common/isolated_world_ids.h"
|
#include "content/public/common/isolated_world_ids.h"
|
||||||
|
@ -429,6 +431,61 @@ std::vector<content::RenderFrameHost*> WebFrameMain::FramesInSubtree() const {
|
||||||
return frame_hosts;
|
return frame_hosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v8::Local<v8::Promise> WebFrameMain::CollectDocumentJSCallStack(
|
||||||
|
gin::Arguments* args) {
|
||||||
|
gin_helper::Promise<base::Value> promise(args->isolate());
|
||||||
|
v8::Local<v8::Promise> handle = promise.GetHandle();
|
||||||
|
|
||||||
|
if (render_frame_disposed_) {
|
||||||
|
promise.RejectWithErrorMessage(
|
||||||
|
"Render frame was disposed before WebFrameMain could be accessed");
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!base::FeatureList::IsEnabled(
|
||||||
|
blink::features::kDocumentPolicyIncludeJSCallStacksInCrashReports)) {
|
||||||
|
promise.RejectWithErrorMessage(
|
||||||
|
"DocumentPolicyIncludeJSCallStacksInCrashReports is not enabled");
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
content::RenderProcessHostImpl* rph_impl =
|
||||||
|
static_cast<content::RenderProcessHostImpl*>(render_frame_->GetProcess());
|
||||||
|
|
||||||
|
rph_impl->GetJavaScriptCallStackGeneratorInterface()
|
||||||
|
->CollectJavaScriptCallStack(
|
||||||
|
base::BindOnce(&WebFrameMain::CollectedJavaScriptCallStack,
|
||||||
|
weak_factory_.GetWeakPtr(), std::move(promise)));
|
||||||
|
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebFrameMain::CollectedJavaScriptCallStack(
|
||||||
|
gin_helper::Promise<base::Value> promise,
|
||||||
|
const std::string& untrusted_javascript_call_stack,
|
||||||
|
const std::optional<blink::LocalFrameToken>& remote_frame_token) {
|
||||||
|
if (render_frame_disposed_) {
|
||||||
|
promise.RejectWithErrorMessage(
|
||||||
|
"Render frame was disposed before call stack was received");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blink::LocalFrameToken& frame_token = render_frame_->GetFrameToken();
|
||||||
|
if (remote_frame_token == frame_token) {
|
||||||
|
base::Value base_value(untrusted_javascript_call_stack);
|
||||||
|
promise.Resolve(base_value);
|
||||||
|
} else if (!remote_frame_token) {
|
||||||
|
// Failed to collect call stack. See logic in:
|
||||||
|
// third_party/blink/renderer/controller/javascript_call_stack_collector.cc
|
||||||
|
promise.Resolve(base::Value());
|
||||||
|
} else {
|
||||||
|
// Requests for call stacks can be initiated on an old RenderProcessHost
|
||||||
|
// then be received after a frame swap.
|
||||||
|
LOG(ERROR) << "Received call stack from old RPH";
|
||||||
|
promise.Resolve(base::Value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void WebFrameMain::DOMContentLoaded() {
|
void WebFrameMain::DOMContentLoaded() {
|
||||||
Emit("dom-ready");
|
Emit("dom-ready");
|
||||||
}
|
}
|
||||||
|
@ -461,6 +518,8 @@ void WebFrameMain::FillObjectTemplate(v8::Isolate* isolate,
|
||||||
v8::Local<v8::ObjectTemplate> templ) {
|
v8::Local<v8::ObjectTemplate> templ) {
|
||||||
gin_helper::ObjectTemplateBuilder(isolate, templ)
|
gin_helper::ObjectTemplateBuilder(isolate, templ)
|
||||||
.SetMethod("executeJavaScript", &WebFrameMain::ExecuteJavaScript)
|
.SetMethod("executeJavaScript", &WebFrameMain::ExecuteJavaScript)
|
||||||
|
.SetMethod("collectJavaScriptCallStack",
|
||||||
|
&WebFrameMain::CollectDocumentJSCallStack)
|
||||||
.SetMethod("reload", &WebFrameMain::Reload)
|
.SetMethod("reload", &WebFrameMain::Reload)
|
||||||
.SetMethod("isDestroyed", &WebFrameMain::IsDestroyed)
|
.SetMethod("isDestroyed", &WebFrameMain::IsDestroyed)
|
||||||
.SetMethod("_send", &WebFrameMain::Send)
|
.SetMethod("_send", &WebFrameMain::Send)
|
||||||
|
|
|
@ -36,6 +36,11 @@ template <typename T>
|
||||||
class Handle;
|
class Handle;
|
||||||
} // namespace gin
|
} // namespace gin
|
||||||
|
|
||||||
|
namespace gin_helper {
|
||||||
|
template <typename T>
|
||||||
|
class Promise;
|
||||||
|
} // namespace gin_helper
|
||||||
|
|
||||||
namespace electron::api {
|
namespace electron::api {
|
||||||
|
|
||||||
class WebContents;
|
class WebContents;
|
||||||
|
@ -128,6 +133,12 @@ class WebFrameMain final : public gin::Wrappable<WebFrameMain>,
|
||||||
std::vector<content::RenderFrameHost*> Frames() const;
|
std::vector<content::RenderFrameHost*> Frames() const;
|
||||||
std::vector<content::RenderFrameHost*> FramesInSubtree() const;
|
std::vector<content::RenderFrameHost*> FramesInSubtree() const;
|
||||||
|
|
||||||
|
v8::Local<v8::Promise> CollectDocumentJSCallStack(gin::Arguments* args);
|
||||||
|
void CollectedJavaScriptCallStack(
|
||||||
|
gin_helper::Promise<base::Value> promise,
|
||||||
|
const std::string& untrusted_javascript_call_stack,
|
||||||
|
const std::optional<blink::LocalFrameToken>& remote_frame_token);
|
||||||
|
|
||||||
void DOMContentLoaded();
|
void DOMContentLoaded();
|
||||||
|
|
||||||
mojo::Remote<mojom::ElectronRenderer> renderer_api_;
|
mojo::Remote<mojom::ElectronRenderer> renderer_api_;
|
||||||
|
|
|
@ -21,8 +21,16 @@ describe('webFrameMain module', () => {
|
||||||
type Server = { server: http.Server, url: string, crossOriginUrl: string }
|
type Server = { server: http.Server, url: string, crossOriginUrl: string }
|
||||||
|
|
||||||
/** Creates an HTTP server whose handler embeds the given iframe src. */
|
/** Creates an HTTP server whose handler embeds the given iframe src. */
|
||||||
const createServer = async (): Promise<Server> => {
|
const createServer = async (options: {
|
||||||
|
headers?: Record<string, string>
|
||||||
|
} = {}): Promise<Server> => {
|
||||||
const server = http.createServer((req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
|
if (options.headers) {
|
||||||
|
for (const [k, v] of Object.entries(options.headers)) {
|
||||||
|
res.setHeader(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams(new URL(req.url || '', `http://${req.headers.host}`).search || '');
|
const params = new URLSearchParams(new URL(req.url || '', `http://${req.headers.host}`).search || '');
|
||||||
if (params.has('frameSrc')) {
|
if (params.has('frameSrc')) {
|
||||||
res.end(`<iframe src="${params.get('frameSrc')}"></iframe>`);
|
res.end(`<iframe src="${params.get('frameSrc')}"></iframe>`);
|
||||||
|
@ -444,6 +452,29 @@ describe('webFrameMain module', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('webFrameMain.collectJavaScriptCallStack', () => {
|
||||||
|
let server: Server;
|
||||||
|
before(async () => {
|
||||||
|
server = await createServer({
|
||||||
|
headers: {
|
||||||
|
'Document-Policy': 'include-js-call-stacks-in-crash-reports'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
after(() => {
|
||||||
|
server.server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('collects call stack during JS execution', async () => {
|
||||||
|
const w = new BrowserWindow({ show: false });
|
||||||
|
await w.loadURL(server.url);
|
||||||
|
const callStackPromise = w.webContents.mainFrame.collectJavaScriptCallStack();
|
||||||
|
w.webContents.mainFrame.executeJavaScript('"run a lil js"');
|
||||||
|
const callStack = await callStackPromise;
|
||||||
|
expect(callStack).to.be.a('string');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('"frame-created" event', () => {
|
describe('"frame-created" event', () => {
|
||||||
it('emits when the main frame is created', async () => {
|
it('emits when the main frame is created', async () => {
|
||||||
const w = new BrowserWindow({ show: false });
|
const w = new BrowserWindow({ show: false });
|
||||||
|
|
|
@ -33,6 +33,12 @@ app.commandLine.appendSwitch('host-resolver-rules', [
|
||||||
'MAP notfound.localhost2 ~NOTFOUND'
|
'MAP notfound.localhost2 ~NOTFOUND'
|
||||||
].join(', '));
|
].join(', '));
|
||||||
|
|
||||||
|
// Enable features required by tests.
|
||||||
|
app.commandLine.appendSwitch('enable-features', [
|
||||||
|
// spec/api-web-frame-main-spec.ts
|
||||||
|
'DocumentPolicyIncludeJSCallStacksInCrashReports'
|
||||||
|
].join(','));
|
||||||
|
|
||||||
global.standardScheme = 'app';
|
global.standardScheme = 'app';
|
||||||
global.zoomScheme = 'zoom';
|
global.zoomScheme = 'zoom';
|
||||||
global.serviceWorkerScheme = 'sw';
|
global.serviceWorkerScheme = 'sw';
|
||||||
|
|
Loading…
Reference in a new issue