Repository: electron/electron
Pull request: feat: suppress devtools console logging

#### Background
Electron app developers often see console errors from the DevTools frontend when running from the command line, for example:

```
[15676:1226/144806.624:ERROR:CONSOLE:1] "Request Autofill.enable failed. {"code":-32601,"message":"'Autofill.enable' wasn't found"}", source: devtools://devtools/bundled/core/protocol_client/protocol_client.js (1)
[15676:1226/144806.624:ERROR:CONSOLE:1] "Request Autofill.setAddresses failed. {"code":-32601,"message":"'Autofill.setAddresses' wasn't found"}", source: devtools://devtools/bundled/core/protocol_client/protocol_client.js (1)
```

These originate upstream in Chromium:

1. The [DevTools frontend](https://source.chromium.org/chromium/chromium/src/+/main:third_party/devtools-frontend/src/front_end/core/protocol_client/InspectorBackend.ts;l=690;drc=9d9a7262df8a7db685524e449c0c08fc3f5f0e8c) logs failed requests for methods not implemented in Electron's Chromium build.
2. The log message is printed by [`content::LogConsoleMessage`](https://source.chromium.org/chromium/chromium/src/+/main:content/browser/log_console_message.cc;l=38;drc=e54f42cb7f0fd77a8f52f3603380b77f65199258) to the native console.

These messages are intended for Chromium/DevTools developers, but they predominantly reach app developers. The console is an impactful surface for dev UX, and [routinely seeing unactionable "errors"](https://en.wikipedia.org/wiki/The_Boy_Who_Cried_Wolf) degrades the experience. (To be clear, it isn't a high priority surface, though.)

#### Solution

Before `content::LogConsoleMessage` writes to the native console, the message passes through [`RenderFrameHostDelegate::DidAddMessageToConsole`](https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/render_frame_host_delegate.h?q=symbol%3A%5Cbcontent%3A%3ARenderFrameHostDelegate%3A%3ADidAddMessageToConsole%5Cb%20case%3Ayes), which is [plumbed](https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/render_frame_host_delegate.h?q=symbol%3A%5Cbcontent%3A%3ARenderFrameHostDelegate%3A%3ADidAddMessageToConsole%5Cb%20case%3Ayes) to [the corresponding WebContents delegate method](https://source.chromium.org/chromium/chromium/src/+/main:content/public/browser/web_contents_delegate.h?q=symbol%3A%5Cbcontent%3A%3AWebContentsDelegate%3A%3ADidAddMessageToConsole%5Cb%20case%3Ayes). That hook can return `true` to indicate the message was handled and suppress the default logging path.

Electron's DevTools frontend is hosted in [`electron::InspectableWebContents`](https://github.com/electron/electron/blob/9eb43f32864014a2d95b5e9226f1031f83075090/shell/browser/ui/inspectable_web_contents.h#L254-L258). This change overrides the delegate `DidAddMessageToConsole` method to suppress the messages from the DevTools' WebContents.

In case we need an escape hatch to see these log messages again, they are reimplemented as verbose logs. Run with `--vmodule=inspectable_web_contents=1` to print out the log messages again. The messages also print as usual in testing builds (which we run in CI).

Fixes #41614.

#### Checklist
- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] relevant API documentation, tutorials, and examples are updated and follow the [documentation style guide](https://github.com/electron/electron/blob/main/docs/development/style-guide.md)
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: DevTools errors are no longer printed to console

Changed files:
filename: shell/browser/ui/inspectable_web_contents.cc
patch: @@ -13,6 +13,7 @@
 
 #include "base/base64.h"
 #include "base/containers/span.h"
+#include "base/dcheck_is_on.h"
 #include "base/memory/raw_ptr.h"
 #include "base/metrics/histogram.h"
 #include "base/strings/pattern.h"
@@ -957,6 +958,35 @@ bool InspectableWebContents::HandleKeyboardEvent(
   return !delegate || delegate->HandleKeyboardEvent(source, event);
 }
 
+bool InspectableWebContents::DidAddMessageToConsole(
+    content::WebContents* source,
+    blink::mojom::ConsoleMessageLevel log_level,
+    const std::u16string& message,
+    int32_t line_no,
+    const std::u16string& source_id) {
+  // Suppress Chromium's default logging of DevTools frontend console messages
+  // into native logs for the managed DevTools WebContents. Can be overridden by
+  // enabling verbose logging.
+  if (source == managed_devtools_web_contents_.get()) {
+#if DCHECK_IS_ON()
+    // In debug/testing builds, let logging through.
+    return false;
+#endif
+
+    if (VLOG_IS_ON(1)) {
+      // Match Chromium's `content::LogConsoleMessage()` output format, but emit
+      // it as a verbose log.
+      logging::LogMessage("CONSOLE", line_no, logging::LOGGING_VERBOSE).stream()
+          << "\"" << message << "\", source: " << source_id << " (" << line_no
+          << ")";
+    }
+
+    return true;  // Suppress the default logging.
+  }
+
+  return false;  // Allow the default logging.
+}
+
 void InspectableWebContents::CloseContents(content::WebContents* source) {
   // This is where the devtools closes itself (by clicking the x button).
   CloseDevTools();

filename: shell/browser/ui/inspectable_web_contents.h
patch: @@ -221,6 +221,11 @@ class InspectableWebContents
   // content::WebContentsDelegate:
   bool HandleKeyboardEvent(content::WebContents*,
                            const input::NativeWebKeyboardEvent&) override;
+  bool DidAddMessageToConsole(content::WebContents* source,
+                              blink::mojom::ConsoleMessageLevel log_level,
+                              const std::u16string& message,
+                              int32_t line_no,
+                              const std::u16string& source_id) override;
   void CloseContents(content::WebContents* source) override;
   std::unique_ptr<content::EyeDropper> OpenEyeDropper(
       content::RenderFrameHost* frame,