🌐 Chrona

Hex.pmHex DocsCILicense: MIT

Manage headless Chrome instances via the Chrome DevTools Protocol.

Chrona 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.

Browse is an internal implementation detail. Configure Chrona through :chrona; Chrona passes the relevant pool options down to Browse under the hood.

📦 Installation

Add chrona to your list of dependencies in mix.exs:

def deps do
  [
    {:chrona, "~> 0.1.0"}
  ]
end

Chrona 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 Chrona:

config :chrona,
  default_pool: MyApp.ChromaPool,
  pools: [
    MyApp.ChromaPool: [pool_size: 4, chrome_path: "/usr/bin/chromium"]
  ]

Then add the configured pools to your supervision tree:

# lib/my_app/application.ex
children = Chrona.children()

Or start a pool directly:

# lib/my_app/application.ex
children = [
  {Chrona.BrowserPool,
   name: MyApp.ChromaPool,
   pool_size: 4,
   chrome_path: "/usr/bin/chromium"}
]

Chrona 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. Chrona.BrowserPool remains the Chrona-facing compatibility wrapper, backed internally by Browse, but its configuration now lives under :chrona.

Check out a browser from a pool

Chrona.checkout(MyApp.ChromaPool, fn browser ->
  # Use Chrona.CDP to interact with the browser
  result =
    with {:ok, ws_url} <- Chrona.Chrome.ws_url(browser) do
      Chrona.CDP.with_session(ws_url, fn cdp ->
        :ok = Chrona.CDP.navigate(cdp, "https://example.com")
        {:ok, screenshot_data} = Chrona.CDP.capture_screenshot(cdp, "jpeg", 90)
        {:ok, Base.decode64!(screenshot_data)}
      end)
    end

  {result, :ok}
end)

Chrona.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:

Chrona.checkout(fn browser ->
  {:ok, browser}
end)

Direct browser management

{:ok, browser} = Chrona.Chrome.start_link()

{:ok, jpeg_binary} = Chrona.Chrome.capture(browser, "<h1>Hello!</h1>", width: 1200, height: 630, quality: 90)

Modules

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 Chrona.CDP.command/3 to call any CDP method directly:

Chrona.checkout(MyApp.ChromaPool, fn browser ->
  result =
    with {:ok, ws_url} <- Chrona.Chrome.ws_url(browser) do
      Chrona.CDP.with_session(ws_url, fn cdp ->
        {:ok, version} = Chrona.CDP.command(cdp, "Browser.getVersion")
        {:ok, version}
      end)
    end

  {result, :ok}
end)

⚙️ Setup

Add Chrona.BrowserPool to your application's supervision tree:

# lib/my_app/application.ex
children = [
  {Chrona.BrowserPool,
   name: MyApp.ChromaPool,
   pool_size: 4,
   chrome_path: "/usr/bin/chromium"}
]

Options:

The :chrona, :pools entries accept the same pool options Chrona passes down to Browse, while keeping the Browse implementation module internal.

Then pass the pool name or pid to Chrona.checkout/3, or use Chrona.checkout/1 with :default_pool configured:

Chrona.checkout(MyApp.ChromaPool, fn browser ->
  {:ok, :done}
end)

📡 Telemetry

Chrona emits Telemetry events for its main lifecycle operations:

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.

:telemetry.attach_many(
  "chrona-logger",
  [
    [:chrona, :checkout, :stop],
    [:chrona, :browser, :capture, :stop],
    [:chrona, :cdp, :command, :stop]
  ],
  fn event, measurements, metadata, _config ->
    IO.inspect({event, measurements, metadata}, label: "chrona.telemetry")
  end,
  nil
)

📄 License

MIT License. See LICENSE for details.