[Chromium Telemetry]-Add the CDP Fetch domain to the inspector

The Fetch domain can be directly applied in the following scenario:

In some third-party pages, the data generated during the testing process
is not exposed for direct access by Telemetry. However, if the frontend
page needs to upload the test data via a network request, Telemetry can
intercept the network request for data reporting through the Fetch
domain and obtain the test data of this session.

Change-Id: Iebe4f43310231b6d55033db61643f53ec79ef2d9
Reviewed-on: https://chromium-review.googlesource.com/c/catapult/+/7030783
Reviewed-by: Mikhail Khokhlov <khokhlov@google.com>
Commit-Queue: Peng Zhou <zhoupeng.1996@bytedance.com>
Reviewed-by: Wenbin Zhang <wenbinzhang@google.com>
diff --git a/telemetry/telemetry/internal/actions/action_runner.py b/telemetry/telemetry/internal/actions/action_runner.py
index bad6281..7908c30 100644
--- a/telemetry/telemetry/internal/actions/action_runner.py
+++ b/telemetry/telemetry/internal/actions/action_runner.py
@@ -49,6 +49,8 @@
 # that Java Heap garbage collection can take ~5 seconds to complete.
 _GARBAGE_COLLECTION_PROPAGATION_TIME = 6
 
+# Timeout for websocket communication when using inspector
+_DEFAULT_WEBSOCKET_TIMEOUT = 60
 
 ActionRunnerBase = six.with_metaclass(trace_event.TracedMetaClass, object)
 
@@ -873,6 +875,43 @@
     """Stops emulation of a mobile device."""
     self._tab.StopMobileDeviceEmulation(timeout)
 
+  def EnableFetch(self,
+                  patterns,
+                  request_paused_callback=None,
+                  auth_required_callback=None,
+                  timeout=_DEFAULT_WEBSOCKET_TIMEOUT):
+    self._tab.EnableFetch(patterns, request_paused_callback,
+                          auth_required_callback, timeout)
+
+  def DisableFetch(self, timeout=_DEFAULT_WEBSOCKET_TIMEOUT):
+    self._tab.DisableFetch(timeout)
+
+  def CreateContinueRequest(self,
+                            request_id,
+                            url=None,
+                            method=None,
+                            post_data=None,
+                            headers=None):
+    return self._tab.CreateContinueRequest(request_id, url, method, post_data,
+                                           headers)
+
+  def ContinueRequestSync(self, request, timeout=_DEFAULT_WEBSOCKET_TIMEOUT):
+    return self._tab.ContinueRequestSync(request, timeout)
+
+  def ContinueRequestAndIgnoreResponse(self, request):
+    self._tab.ContinueRequestAndIgnoreResponse(request)
+
+  def CreateContinueWithAuthRequest(self, request_id, auth_challenge_response):
+    return self._tab.CreateContinueWithAuthRequest(request_id,
+                                                   auth_challenge_response)
+
+  def ContinueWithAuthRequestSync(self,
+                                  request,
+                                  timeout=_DEFAULT_WEBSOCKET_TIMEOUT):
+    return self._tab.ContinueWithAuthRequestSync(request, timeout)
+
+  def ContinueWithAuthRequestAndIgnoreResponse(self, request):
+    self._tab.ContinueWithAuthRequestAndIgnoreResponse(request)
 
 class Interaction():
 
diff --git a/telemetry/telemetry/internal/backends/chrome_inspector/inspector_backend.py b/telemetry/telemetry/internal/backends/chrome_inspector/inspector_backend.py
index 6f31366..726ffa3 100644
--- a/telemetry/telemetry/internal/backends/chrome_inspector/inspector_backend.py
+++ b/telemetry/telemetry/internal/backends/chrome_inspector/inspector_backend.py
@@ -19,6 +19,7 @@
 from telemetry import decorators
 from telemetry.internal.backends.chrome_inspector import devtools_http
 from telemetry.internal.backends.chrome_inspector import inspector_console
+from telemetry.internal.backends.chrome_inspector import inspector_fetch
 from telemetry.internal.backends.chrome_inspector import inspector_log
 from telemetry.internal.backends.chrome_inspector import inspector_memory
 from telemetry.internal.backends.chrome_inspector import inspector_page
@@ -78,6 +79,7 @@
     try:
       self._websocket.Connect(self.debugger_url, timeout)
       self._console = inspector_console.InspectorConsole(self._websocket)
+      self._fetch = inspector_fetch.InspectorFetch(self._websocket)
       self._log = inspector_log.InspectorLog(self._websocket)
       self._memory = inspector_memory.InspectorMemory(self._websocket)
       self._runtime = inspector_runtime.InspectorRuntime(self._websocket)
@@ -945,3 +947,47 @@
   @_HandleInspectorWebSocketExceptions
   def CollectGarbage(self, timeout_in_seconds=60):
     self._page.CollectGarbage(timeout_in_seconds)
+
+  @_HandleInspectorWebSocketExceptions
+  def EnableFetch(self,
+                  patterns,
+                  request_paused_callback=None,
+                  auth_required_callback=None,
+                  timeout=60):
+    self._fetch.EnableFetch(patterns, request_paused_callback,
+                            auth_required_callback, timeout)
+
+  @_HandleInspectorWebSocketExceptions
+  def DisableFetch(self, timeout=60):
+    self._fetch.DisableFetch(timeout)
+
+  @_HandleInspectorWebSocketExceptions
+  def CreateContinueRequest(self,
+                            request_id,
+                            url=None,
+                            method=None,
+                            post_data=None,
+                            headers=None):
+    return self._fetch.CreateContinueRequest(request_id, url, method, post_data,
+                                             headers)
+
+  @_HandleInspectorWebSocketExceptions
+  def ContinueRequestSync(self, request, timeout=60):
+    return self._fetch.ContinueRequestSync(request, timeout)
+
+  @_HandleInspectorWebSocketExceptions
+  def ContinueRequestAndIgnoreResponse(self, request):
+    self._fetch.ContinueRequestAndIgnoreResponse(request)
+
+  @_HandleInspectorWebSocketExceptions
+  def CreateContinueWithAuthRequest(self, request_id, auth_challenge_response):
+    return self._fetch.CreateContinueWithAuthRequest(request_id,
+                                                     auth_challenge_response)
+
+  @_HandleInspectorWebSocketExceptions
+  def ContinueWithAuthRequestSync(self, request, timeout=60):
+    return self._fetch.ContinueWithAuthRequestSync(request, timeout)
+
+  @_HandleInspectorWebSocketExceptions
+  def ContinueWithAuthRequestAndIgnoreResponse(self, request):
+    self._fetch.ContinueWithAuthRequestAndIgnoreResponse(request)
diff --git a/telemetry/telemetry/internal/backends/chrome_inspector/inspector_fetch.py b/telemetry/telemetry/internal/backends/chrome_inspector/inspector_fetch.py
new file mode 100644
index 0000000..88de648
--- /dev/null
+++ b/telemetry/telemetry/internal/backends/chrome_inspector/inspector_fetch.py
@@ -0,0 +1,126 @@
+# Copyright 2025 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from __future__ import absolute_import
+
+from telemetry.core import exceptions
+
+# Timeout for websocket communication when using inspector
+_DEFAULT_WEBSOCKET_TIMEOUT = 60
+
+
+class InspectorFetchException(exceptions.Error):
+  pass
+
+
+class InspectorFetch():
+  """
+  Reference: https://chromedevtools.github.io/devtools-protocol/tot/Fetch/
+  """
+
+  def __init__(self, inspector_websocket):
+    self._inspector_websocket = inspector_websocket
+    self._inspector_websocket.RegisterDomain('Fetch', self._OnNotification)
+    self._request_paused_callback = None
+    self._auth_required_callback = None
+
+  def EnableFetch(self,
+                  patterns,
+                  request_paused_callback=None,
+                  auth_required_callback=None,
+                  timeout=60):
+    self._request_paused_callback = request_paused_callback
+    self._auth_required_callback = auth_required_callback
+    self._EnableFetch(patterns, timeout)
+
+  def DisableFetch(self, timeout=60):
+    self._DisableFetch(timeout)
+    self._request_paused_callback = None
+    self._auth_required_callback = None
+
+  def _OnNotification(self, message):
+    if message['method'] == 'Fetch.requestPaused':
+      if self._request_paused_callback:
+        self._request_paused_callback(message)
+
+    if message['method'] == 'Fetch.authRequired':
+      if self._auth_required_callback:
+        self._auth_required_callback(message)
+
+  def _EnableFetch(self, patterns, timeout):
+    request = {'method': 'Fetch.enable', 'params': {}}
+
+    if patterns:
+      params = {'patterns': patterns}
+      request['params'] = params
+    if self._auth_required_callback:
+      request['params']['handleAuthChallenge'] = True
+
+    res = self._inspector_websocket.SyncRequest(request, timeout)
+
+    if 'error' in res:
+      raise InspectorFetchException(res['error']['message'])
+
+  def _DisableFetch(self, timeout):
+    request = {'method': 'Fetch.disable'}
+    res = self._inspector_websocket.SyncRequest(request, timeout)
+
+    if 'error' in res:
+      raise InspectorFetchException(res['error']['message'])
+
+  def CreateContinueRequest(self,
+                            request_id,
+                            url=None,
+                            method=None,
+                            post_data=None,
+                            headers=None):
+    request = {
+        'method': 'Fetch.continueRequest',
+        'params': {
+            'requestId': request_id,
+        }
+    }
+    if url:
+      request['params']['url'] = url
+    if method:
+      request['params']['method'] = method
+    if post_data:
+      request['params']['postData'] = post_data
+    if headers:
+      request['params']['headers'] = headers
+
+    return request
+
+  def ContinueRequestSync(self, request, timeout=_DEFAULT_WEBSOCKET_TIMEOUT):
+    res = self._inspector_websocket.SyncRequest(request, timeout)
+
+    if 'error' in res:
+      raise InspectorFetchException(res['error']['message'])
+    return res
+
+  def ContinueRequestAndIgnoreResponse(self, request):
+    self._inspector_websocket.SendAndIgnoreResponse(request)
+
+  def CreateContinueWithAuthRequest(self, request_id, auth_challenge_response):
+    request = {
+        'method': 'Fetch.continueWithAuth',
+        'params': {
+            'requestId': request_id,
+            'authChallengeResponse': auth_challenge_response,
+        }
+    }
+
+    return request
+
+  def ContinueWithAuthRequestSync(self,
+                                  request,
+                                  timeout=_DEFAULT_WEBSOCKET_TIMEOUT):
+    res = self._inspector_websocket.SyncRequest(request, timeout)
+
+    if 'error' in res:
+      raise InspectorFetchException(res['error']['message'])
+    return res
+
+  def ContinueWithAuthRequestAndIgnoreResponse(self, request):
+    self._inspector_websocket.SendAndIgnoreResponse(request)
diff --git a/telemetry/telemetry/internal/browser/tab.py b/telemetry/telemetry/internal/browser/tab.py
index b42c381..e1daf9a 100644
--- a/telemetry/telemetry/internal/browser/tab.py
+++ b/telemetry/telemetry/internal/browser/tab.py
@@ -301,3 +301,39 @@
     return self._inspector_backend.WaitForSharedStorageEvents(expected_events,
                                                               mode,
                                                               timeout)
+
+  def EnableFetch(self,
+                  patterns,
+                  request_paused_callback=None,
+                  auth_required_callback=None,
+                  timeout=DEFAULT_TAB_TIMEOUT):
+    self._inspector_backend.EnableFetch(patterns, request_paused_callback,
+                                        auth_required_callback, timeout)
+
+  def DisableFetch(self, timeout=DEFAULT_TAB_TIMEOUT):
+    self._inspector_backend.DisableFetch(timeout)
+
+  def CreateContinueRequest(self,
+                            request_id,
+                            url=None,
+                            method=None,
+                            post_data=None,
+                            headers=None):
+    return self._inspector_backend.CreateContinueRequest(
+        request_id, url, method, post_data, headers)
+
+  def ContinueRequestSync(self, request, timeout=DEFAULT_TAB_TIMEOUT):
+    return self._inspector_backend.ContinueRequestSync(request, timeout)
+
+  def ContinueRequestAndIgnoreResponse(self, request):
+    self._inspector_backend.ContinueRequestAndIgnoreResponse(request)
+
+  def CreateContinueWithAuthRequest(self, request_id, auth_challenge_response):
+    return self._inspector_backend.CreateContinueWithAuthRequest(
+        request_id, auth_challenge_response)
+
+  def ContinueWithAuthRequestSync(self, request, timeout=DEFAULT_TAB_TIMEOUT):
+    return self._inspector_backend.ContinueWithAuthRequestSync(request, timeout)
+
+  def ContinueWithAuthRequestAndIgnoreResponse(self, request):
+    self._inspector_backend.ContinueWithAuthRequestAndIgnoreResponse(request)