Skip to main content
Browser monitors use Playwright to launch a real browser and interact with your web pages. Use them to monitor login flows, verify UI content, catch JavaScript errors, and test anything that requires a browser. Browser monitors work with both the Sequential Builder and Graph Builder, and can be mixed with API requests in the same monitor.

Basic example

import {
  createMonitorBuilder, Frequency, Assert,
  BrowserAction, navigate, waitForSelector, extractText,
} from "@griffin-app/griffin";

const monitor = createMonitorBuilder({
  name: "landing-page",
  frequency: Frequency.every(5).minutes(),
})
  .browser("load_page", BrowserAction({
    browser: "chromium",
    steps: [
      navigate("https://www.example.com"),
      waitForSelector("h1"),
      extractText("heading", "h1"),
    ],
  }))
  .assert((state) => [
    Assert(state["load_page"].page.url).contains("example.com"),
    Assert(state["load_page"].page.title).contains("Example"),
    Assert(state["load_page"].extracts["heading"]).equals("Welcome"),
    Assert(state["load_page"].console.errors).isEmpty(),
  ])
  .build();

export default monitor;

Browser steps

Each browser node contains an ordered list of steps. Steps execute sequentially inside a single browser page. Navigate to a URL. Accepts a raw string, variable(), or secret().
navigate("https://example.com")
navigate("https://example.com", { wait_until: "networkidle" })
navigate(variable("APP_URL"))
Options:
OptionValuesDefault
wait_until"load", "domcontentloaded", "networkidle", "commit""load"

click

Click an element by CSS selector.
click("[data-testid='submit']")
click("button.menu", { button: "right", click_count: 2 })
Options:
OptionValuesDefault
button"left", "right", "middle""left"
click_countnumber1

fill

Type into an input field. The value accepts raw strings, variable(), or secret().
fill("#email", "test@example.com")
fill("#password", secret("TEST_PASSWORD"))
fill("#api-url", variable("API_BASE"))

select

Choose an option from a <select> dropdown.
select("#country", "US")
select("#plan", variable("DEFAULT_PLAN"))

waitForSelector

Wait for an element to reach a given state before continuing.
waitForSelector("[data-testid='dashboard']")
waitForSelector(".loading", { state: "hidden", timeout_ms: 10000 })
Options:
OptionValuesDefault
state"visible", "hidden", "attached", "detached""visible"
timeout_msnumber (milliseconds)executor default

extractText

Extract the text content of an element and store it under a name for later assertions.
extractText("heading", "h1")
extractText("price", ".product-price")
extractText("error_message", "[data-testid='error']")
Access extracted values in assertions with state["node"].extracts["name"].

screenshot

Capture a screenshot of the page at this point in the flow.
screenshot()

BrowserAction config

Wrap your steps in BrowserAction() to create a browser node:
BrowserAction({
  browser: "chromium",                        // Optional: browser engine
  viewport: { width: 1920, height: 1080 },   // Optional: viewport size
  steps: [                                    // Required: ordered steps
    navigate("https://example.com"),
    click("button"),
  ],
})
FieldTypeDefaultDescription
browser"chromium", "firefox", "webkit""chromium"Browser engine to use
viewport{ width: number, height: number }Browser defaultViewport dimensions
stepsBrowserStep[]Ordered list of browser steps

Browser assertions

Browser nodes expose different state properties than API request nodes. Use these in .assert():

Page state

Assert(state["node"].page.url).contains("/dashboard")
Assert(state["node"].page.title).equals("Dashboard")

Performance

Assert(state["node"].duration).lessThan(5000)

Console errors

Catch JavaScript errors thrown in the browser:
Assert(state["node"].console.errors).isEmpty()

Extracted text

Assert on values captured by extractText() steps:
Assert(state["node"].extracts["heading"]).equals("Welcome")
Assert(state["node"].extracts["price"]).contains("$")
Assert(state["node"].extracts["status"]).not.equals("error")
All standard assertion predicates and negation work with browser subjects.

Login flow example

A common pattern: navigate to a login page, enter credentials using secret(), submit, and verify the redirect.
import {
  createMonitorBuilder, Frequency, Assert,
  BrowserAction, navigate, fill, click,
  waitForSelector, screenshot, secret,
} from "@griffin-app/griffin";

const monitor = createMonitorBuilder({
  name: "login-flow",
  frequency: Frequency.every(30).minutes(),
})
  .browser("login", BrowserAction({
    steps: [
      navigate("https://app.example.com/login"),
      fill("#email", "test@example.com"),
      fill("#password", secret("TEST_PASSWORD")),
      click("[data-testid='submit']"),
      waitForSelector("[data-testid='dashboard']"),
      screenshot(),
    ],
  }))
  .assert((state) => [
    Assert(state["login"].page.url).contains("/dashboard"),
    Assert(state["login"].console.errors).isEmpty(),
    Assert(state["login"].duration).lessThan(10000),
  ])
  .build();

export default monitor;

Mixed API + browser monitors

Combine API requests and browser interactions in a single monitor. For example, create a resource via the API and then verify it appears in the UI:
import {
  createMonitorBuilder, POST, Json, Frequency, Assert,
  BrowserAction, navigate, waitForSelector, extractText, variable,
} from "@griffin-app/griffin";

const monitor = createMonitorBuilder({
  name: "create-and-verify",
  frequency: Frequency.every(1).hour(),
})
  .request("create_item", {
    method: POST,
    base: variable("api-service"),
    path: "/api/v1/items",
    response_format: Json,
    body: { name: "test-item" },
  })
  .assert((state) => [
    Assert(state["create_item"].status).equals(201),
  ])
  .browser("verify_in_ui", BrowserAction({
    steps: [
      navigate("https://app.example.com/items"),
      waitForSelector(".item-list"),
      extractText("item_name", ".item-list .item:first-child .name"),
    ],
  }))
  .assert((state) => [
    Assert(state["verify_in_ui"].extracts["item_name"]).equals("test-item"),
  ])
  .build();

export default monitor;

Using with the graph builder

Use BrowserAction() with addNode() the same way you use HttpRequest():
import {
  createGraphBuilder, BrowserAction, navigate, click,
  START, END, Frequency,
} from "@griffin-app/griffin";

const monitor = createGraphBuilder({
  name: "browser-graph",
  frequency: Frequency.every(30).minutes(),
})
  .addNode("check_page", BrowserAction({
    browser: "chromium",
    steps: [
      navigate("https://example.com"),
      click("[data-testid='cta']"),
    ],
  }))
  .addEdge(START, "check_page")
  .addEdge("check_page", END)
  .build();

export default monitor;

Frequency requirements

Browser monitors require a minimum frequency of 1 minute. API-only monitors have no minimum.
// ✅ Valid
Frequency.every(1).minutes()
Frequency.every(5).minutes()
Frequency.every(1).hour()

// ❌ Throws at build time
Frequency.every(30).seconds()   // less than 1 minute
See Frequencies & Scheduling for more on scheduling.
Browser monitors are more resource-intensive than API monitors. Choose a frequency that balances monitoring coverage with execution cost.