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:
trop[bot] 2024-12-03 09:55:43 -08:00 committed by GitHub
parent 7d2d5240bd
commit e1d9df224c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 131 additions and 1 deletions

View file

@ -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
#### `frame.ipc` _Readonly_

View file

@ -9,9 +9,11 @@
#include <utility>
#include <vector>
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#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/render_frame_host.h"
#include "content/public/common/isolated_world_ids.h"
@ -429,6 +431,61 @@ std::vector<content::RenderFrameHost*> WebFrameMain::FramesInSubtree() const {
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() {
Emit("dom-ready");
}
@ -461,6 +518,8 @@ void WebFrameMain::FillObjectTemplate(v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> templ) {
gin_helper::ObjectTemplateBuilder(isolate, templ)
.SetMethod("executeJavaScript", &WebFrameMain::ExecuteJavaScript)
.SetMethod("collectJavaScriptCallStack",
&WebFrameMain::CollectDocumentJSCallStack)
.SetMethod("reload", &WebFrameMain::Reload)
.SetMethod("isDestroyed", &WebFrameMain::IsDestroyed)
.SetMethod("_send", &WebFrameMain::Send)

View file

@ -36,6 +36,11 @@ template <typename T>
class Handle;
} // namespace gin
namespace gin_helper {
template <typename T>
class Promise;
} // namespace gin_helper
namespace electron::api {
class WebContents;
@ -128,6 +133,12 @@ class WebFrameMain final : public gin::Wrappable<WebFrameMain>,
std::vector<content::RenderFrameHost*> Frames() 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();
mojo::Remote<mojom::ElectronRenderer> renderer_api_;

View file

@ -21,8 +21,16 @@ describe('webFrameMain module', () => {
type Server = { server: http.Server, url: string, crossOriginUrl: string }
/** 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) => {
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 || '');
if (params.has('frameSrc')) {
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', () => {
it('emits when the main frame is created', async () => {
const w = new BrowserWindow({ show: false });

View file

@ -33,6 +33,12 @@ app.commandLine.appendSwitch('host-resolver-rules', [
'MAP notfound.localhost2 ~NOTFOUND'
].join(', '));
// Enable features required by tests.
app.commandLine.appendSwitch('enable-features', [
// spec/api-web-frame-main-spec.ts
'DocumentPolicyIncludeJSCallStacksInCrashReports'
].join(','));
global.standardScheme = 'app';
global.zoomScheme = 'zoom';
global.serviceWorkerScheme = 'sw';