# 🌐 BrowseChrome
[](https://hex.pm/packages/browse_chrome)
[](https://hexdocs.pm/browse_chrome)
[](https://github.com/pepicrft/browse_chrome/actions/workflows/browse_chrome.yml)
[](https://opensource.org/licenses/MIT)
Manage headless Chrome instances via the Chrome DevTools Protocol.
BrowseChrome provides a pool of warm headless Chrome/Chromium instances managed through a supervision tree, ready for use via the Chrome DevTools Protocol. It handles Chrome lifecycle and CDP WebSocket communication directly, and now delegates shared browser interface and pool runtime responsibilities to [`Browse`](https://hex.pm/packages/browse).
`Browse` is an internal implementation detail. Configure BrowseChrome through `:browse_chrome`; BrowseChrome passes the relevant pool options down to `Browse` under the hood.
## 📦 Installation
Add `browse_chrome` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:browse_chrome, "~> 0.1.0"}
]
end
```
BrowseChrome requires Chrome or Chromium to be installed on the system. It will auto-detect common installation paths, or you can configure it explicitly.
## 🚀 Usage
### Add a pool to your supervision tree
Configure pools through BrowseChrome:
```elixir
config :browse_chrome,
default_pool: MyApp.ChromaPool,
pools: [
MyApp.ChromaPool: [pool_size: 4, chrome_path: "/usr/bin/chromium"]
]
```
Then add the configured pools to your supervision tree:
```elixir
# lib/my_app/application.ex
children = BrowseChrome.children()
```
Or start a pool directly:
```elixir
# lib/my_app/application.ex
children = [
{BrowseChrome.BrowserPool,
name: MyApp.ChromaPool,
pool_size: 4,
chrome_path: "/usr/bin/chromium"}
]
```
BrowseChrome does not start a pool for you. The consumer owns pool supervision and decides how many pools to run, how they are named, and where they live in the supervision tree. `BrowseChrome.BrowserPool` remains the BrowseChrome-facing compatibility wrapper, backed internally by `Browse`, but its configuration now lives under `:browse_chrome`.
### Check out a browser from a pool
```elixir
BrowseChrome.checkout(MyApp.ChromaPool, fn browser ->
# Use BrowseChrome.CDP to interact with the browser
result =
with {:ok, ws_url} <- BrowseChrome.Chrome.ws_url(browser) do
BrowseChrome.CDP.with_session(ws_url, fn cdp ->
:ok = BrowseChrome.CDP.navigate(cdp, "https://example.com")
{:ok, screenshot_data} = BrowseChrome.CDP.capture_screenshot(cdp, "jpeg", 90)
{:ok, Base.decode64!(screenshot_data)}
end)
end
{result, :ok}
end)
```
`BrowseChrome.CDP.with_session/2` is the recommended API. It guarantees the WebSocket is disconnected even if your callback raises or returns early.
If you configured `default_pool`, you can omit the pool name:
```elixir
BrowseChrome.checkout(fn browser ->
{:ok, browser}
end)
```
### Direct browser management
```elixir
{:ok, browser} = BrowseChrome.Chrome.start_link()
{:ok, jpeg_binary} = BrowseChrome.Chrome.capture(browser, "<h1>Hello!</h1>", width: 1200, height: 630, quality: 90)
```
### Modules
- `BrowseChrome.Chrome` - GenServer managing a single headless Chrome instance
- `BrowseChrome.CDP` - WebSocket client for the Chrome DevTools Protocol
- `BrowseChrome.Browser` - `Browse.Browser` adapter built on BrowseChrome's Chrome worker
- `BrowseChrome.BrowserPool` - BrowseChrome-facing pool wrapper backed by `Browse`
### Use the full CDP surface
The convenience helpers cover common tasks like navigation, viewport setup, and screenshots, but Chrome exposes many more methods than those wrappers.
Use `BrowseChrome.CDP.command/3` to call any CDP method directly:
```elixir
BrowseChrome.checkout(MyApp.ChromaPool, fn browser ->
result =
with {:ok, ws_url} <- BrowseChrome.Chrome.ws_url(browser) do
BrowseChrome.CDP.with_session(ws_url, fn cdp ->
{:ok, version} = BrowseChrome.CDP.command(cdp, "Browser.getVersion")
{:ok, version}
end)
end
{result, :ok}
end)
```
## ⚙️ Setup
Add `BrowseChrome.BrowserPool` to your application's supervision tree:
```elixir
# lib/my_app/application.ex
children = [
{BrowseChrome.BrowserPool,
name: MyApp.ChromaPool,
pool_size: 4,
chrome_path: "/usr/bin/chromium"}
]
```
Options:
- `:name` - pool name used when checking out browsers
- `:pool_size` - number of warm Chrome instances (default: `2`)
- `:chrome_path` - path to Chrome/Chromium binary (auto-detected if omitted)
The `:browse_chrome, :pools` entries accept the same pool options BrowseChrome passes down to `Browse`, while keeping the `Browse` implementation module internal.
Then pass the pool name or pid to `BrowseChrome.checkout/3`, or use `BrowseChrome.checkout/1` with `:default_pool` configured:
```elixir
BrowseChrome.checkout(MyApp.ChromaPool, fn browser ->
{:ok, :done}
end)
```
## 📡 Telemetry
BrowseChrome emits [Telemetry](https://hexdocs.pm/telemetry) events for its main lifecycle operations:
- `[:browse_chrome, :checkout, :start | :stop | :exception]`
- `[:browse_chrome, :browser, :init, :start | :stop | :exception]`
- `[:browse_chrome, :browser, :capture, :start | :stop | :exception]`
- `[:browse_chrome, :cdp, :connect, :start | :stop | :exception]`
- `[:browse_chrome, :cdp, :disconnect, :start | :stop | :exception]`
- `[:browse_chrome, :cdp, :command, :start | :stop | :exception]`
Stop and exception events include a `:duration` measurement in native time units. CDP command events include the `:method` metadata field, and browser capture events include `:width`, `:height`, and `:quality`.
```elixir
:telemetry.attach_many(
"browse_chrome-logger",
[
[:browse_chrome, :checkout, :stop],
[:browse_chrome, :browser, :capture, :stop],
[:browse_chrome, :cdp, :command, :stop]
],
fn event, measurements, metadata, _config ->
IO.inspect({event, measurements, metadata}, label: "browse_chrome.telemetry")
end,
nil
)
```
## 📄 License
MIT License. See [LICENSE](LICENSE) for details.