_tracing.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. # Copyright (c) Microsoft Corporation.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import pathlib
  15. from typing import Any, Dict, Literal, Optional, Pattern, Union, cast
  16. from playwright._impl._api_structures import TracingGroupLocation
  17. from playwright._impl._artifact import Artifact
  18. from playwright._impl._connection import (
  19. ChannelOwner,
  20. from_channel,
  21. from_nullable_channel,
  22. )
  23. from playwright._impl._disposable import DisposableStub
  24. from playwright._impl._helper import Error, locals_to_params
  25. class Tracing(ChannelOwner):
  26. def __init__(
  27. self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
  28. ) -> None:
  29. super().__init__(parent, type, guid, initializer)
  30. self._include_sources: bool = False
  31. self._is_live: bool = False
  32. self._stacks_id: Optional[str] = None
  33. self._is_tracing: bool = False
  34. self._traces_dir: Optional[str] = None
  35. self._har_id: Optional[str] = None
  36. self._har_recorders: Dict[str, Dict[str, str]] = {}
  37. async def start(
  38. self,
  39. name: str = None,
  40. title: str = None,
  41. snapshots: bool = None,
  42. screenshots: bool = None,
  43. sources: bool = None,
  44. live: bool = None,
  45. ) -> None:
  46. params = locals_to_params(locals())
  47. self._include_sources = bool(sources)
  48. self._is_live = bool(live)
  49. await self._channel.send(
  50. "tracingStart",
  51. None,
  52. {
  53. "name": name,
  54. "snapshots": snapshots,
  55. "screenshots": screenshots,
  56. "live": live,
  57. },
  58. )
  59. trace_name = await self._channel.send(
  60. "tracingStartChunk", None, {"title": title, "name": name}
  61. )
  62. await self._start_collecting_stacks(trace_name)
  63. async def start_chunk(self, title: str = None, name: str = None) -> None:
  64. params = locals_to_params(locals())
  65. trace_name = await self._channel.send("tracingStartChunk", None, params)
  66. await self._start_collecting_stacks(trace_name)
  67. async def _start_collecting_stacks(self, trace_name: str) -> None:
  68. if not self._is_tracing:
  69. self._is_tracing = True
  70. self._connection.set_is_tracing(True)
  71. self._stacks_id = await self._connection.local_utils.tracing_started(
  72. self._traces_dir, trace_name, self._is_live
  73. )
  74. async def stop_chunk(self, path: Union[pathlib.Path, str] = None) -> None:
  75. await self._do_stop_chunk(path)
  76. async def stop(self, path: Union[pathlib.Path, str] = None) -> None:
  77. await self._do_stop_chunk(path)
  78. await self._channel.send(
  79. "tracingStop",
  80. None,
  81. )
  82. async def _do_stop_chunk(self, file_path: Union[pathlib.Path, str] = None) -> None:
  83. self._reset_stack_counter()
  84. if not file_path:
  85. # Not interested in any artifacts
  86. await self._channel.send("tracingStopChunk", None, {"mode": "discard"})
  87. if self._stacks_id:
  88. await self._connection.local_utils.trace_discarded(self._stacks_id)
  89. return
  90. is_local = not self._connection.is_remote
  91. if is_local:
  92. result = await self._channel.send_return_as_dict(
  93. "tracingStopChunk", None, {"mode": "entries"}
  94. )
  95. await self._connection.local_utils.zip(
  96. {
  97. "zipFile": str(file_path),
  98. "entries": result["entries"],
  99. "stacksId": self._stacks_id,
  100. "mode": "write",
  101. "includeSources": self._include_sources,
  102. }
  103. )
  104. return
  105. result = await self._channel.send_return_as_dict(
  106. "tracingStopChunk",
  107. None,
  108. {
  109. "mode": "archive",
  110. },
  111. )
  112. artifact = cast(
  113. Optional[Artifact],
  114. from_nullable_channel(result.get("artifact")),
  115. )
  116. # The artifact may be missing if the browser closed while stopping tracing.
  117. if not artifact:
  118. if self._stacks_id:
  119. await self._connection.local_utils.trace_discarded(self._stacks_id)
  120. return
  121. # Save trace to the final local file.
  122. await artifact.save_as(file_path)
  123. await artifact.delete()
  124. await self._connection.local_utils.zip(
  125. {
  126. "zipFile": str(file_path),
  127. "entries": [],
  128. "stacksId": self._stacks_id,
  129. "mode": "append",
  130. "includeSources": self._include_sources,
  131. }
  132. )
  133. def _reset_stack_counter(self) -> None:
  134. if self._is_tracing:
  135. self._is_tracing = False
  136. self._connection.set_is_tracing(False)
  137. async def group(
  138. self, name: str, location: TracingGroupLocation = None
  139. ) -> DisposableStub:
  140. await self._channel.send("tracingGroup", None, locals_to_params(locals()))
  141. return DisposableStub(lambda: self.group_end(), self)
  142. async def group_end(self) -> None:
  143. await self._channel.send(
  144. "tracingGroupEnd",
  145. None,
  146. )
  147. async def start_har(
  148. self,
  149. path: Union[pathlib.Path, str],
  150. content: Literal["attach", "embed", "omit"] = None,
  151. mode: Literal["full", "minimal"] = None,
  152. urlFilter: Union[str, Pattern[str]] = None,
  153. ) -> DisposableStub:
  154. if self._har_id:
  155. raise Error("HAR recording has already been started")
  156. is_zip = str(path).endswith(".zip")
  157. default_content: Literal["attach", "embed", "omit"] = (
  158. "attach" if is_zip else "embed"
  159. )
  160. self._har_id = await self._record_into_har(
  161. har=path,
  162. page=None,
  163. url=urlFilter,
  164. update_content=content or default_content,
  165. update_mode=mode or "full",
  166. )
  167. return DisposableStub(lambda: self.stop_har(), self)
  168. async def _record_into_har(
  169. self,
  170. har: Union[pathlib.Path, str],
  171. page: Optional[ChannelOwner],
  172. url: Union[str, Pattern[str]] = None,
  173. update_content: Literal["attach", "embed", "omit"] = None,
  174. update_mode: Literal["full", "minimal"] = None,
  175. resourcesDir: Optional[str] = None,
  176. ) -> str:
  177. is_zip = str(har).endswith(".zip")
  178. url_glob: Optional[str] = None
  179. url_regex_source: Optional[str] = None
  180. url_regex_flags: Optional[str] = None
  181. if isinstance(url, str):
  182. url_glob = url
  183. elif url is not None:
  184. url_regex_source = url.pattern
  185. url_regex_flags = "".join(
  186. flag
  187. for flag, mask in (("i", 2), ("m", 8), ("s", 16))
  188. if url.flags & mask
  189. )
  190. options: Dict[str, object] = {
  191. "content": update_content or "attach",
  192. "mode": update_mode or "minimal",
  193. "harPath": None if is_zip else str(har),
  194. }
  195. if url_glob is not None:
  196. options["urlGlob"] = url_glob
  197. if url_regex_source is not None:
  198. options["urlRegexSource"] = url_regex_source
  199. if url_regex_flags is not None:
  200. options["urlRegexFlags"] = url_regex_flags
  201. if resourcesDir is not None:
  202. options["resourcesDir"] = resourcesDir
  203. params: Dict[str, Any] = {"options": options}
  204. if page is not None:
  205. params["page"] = page._channel
  206. result = await self._channel.send_return_as_dict("harStart", None, params)
  207. har_id = result["harId"]
  208. self._har_recorders[har_id] = {"path": str(har)}
  209. return har_id
  210. async def _export_all_hars(self) -> None:
  211. for har_id in list(self._har_recorders.keys()):
  212. await self._export_har(har_id)
  213. self._har_id = None
  214. async def stop_har(self) -> None:
  215. har_id = self._har_id
  216. if not har_id:
  217. return
  218. self._har_id = None
  219. await self._export_har(har_id)
  220. async def _export_har(self, har_id: str) -> None:
  221. params = self._har_recorders.pop(har_id, None)
  222. if not params:
  223. return
  224. is_local = not self._connection.is_remote
  225. is_zip = params["path"].endswith(".zip")
  226. if is_local:
  227. result = await self._channel.send_return_as_dict(
  228. "harExport", None, {"harId": har_id, "mode": "entries"}
  229. )
  230. if not is_zip:
  231. # Server wrote HAR and resources to the user's chosen paths.
  232. return
  233. await self._connection.local_utils.zip(
  234. {
  235. "zipFile": params["path"],
  236. "entries": result["entries"],
  237. "mode": "write",
  238. "includeSources": False,
  239. }
  240. )
  241. return
  242. result = await self._channel.send_return_as_dict(
  243. "harExport", None, {"harId": har_id, "mode": "archive"}
  244. )
  245. artifact = cast(Artifact, from_channel(result["artifact"]))
  246. if is_zip:
  247. await artifact.save_as(params["path"])
  248. await artifact.delete()
  249. return
  250. # Uncompressed har is not supported in thin clients
  251. await artifact.save_as(params["path"] + ".tmp")
  252. await artifact.delete()