> ## Documentation Index
> Fetch the complete documentation index at: https://docs.uplink.build/llms.txt
> Use this file to discover all available pages before exploring further.

# Events

> Listen to page events and lifecycle hooks

Monitor page events including HTTP requests, navigation, DOM changes, and custom events emitted by the page.

## Methods

### `page.on()`

Listens for page events. Returns an AbortController for cleanup.

```typescript theme={null}
page.on(event: string, handler: EventListener): Promise<AbortController>
```

**Parameters:**

* `event`: Event type to listen for
* `handler`: Function to handle the event

**Returns:** `Promise<AbortController>` - Controller for stopping the listener

**Example:**

```typescript theme={null}
const controller = await page.on('xhr', async (event) => {
  console.log('HTTP Request:', event.data.url)
})

// Later, stop listening
await page.off(controller)
```

### `page.once()`

Listens for a page event once, then automatically removes the listener.

```typescript theme={null}
page.once(event: string, handler: EventListener): Promise<AbortController>
```

**Parameters:**

* `event`: Event type to listen for
* `handler`: Function to handle the event

**Returns:** `Promise<AbortController>` - Controller (listener auto-removed after first event)

**Example:**

```typescript theme={null}
await page.once('finished', async (event) => {
  console.log('Page finished loading')
})
```

### `page.off()`

Stops listening to events by providing the AbortController.

```typescript theme={null}
page.off(controller: AbortController): void
```

**Parameters:**

* `controller`: AbortController returned from `on()` or `once()`

**Example:**

```typescript theme={null}
const controller = await page.on('domchange', handler)
await page.off(controller)
```

## Available events

### `xhr`

Emitted when an HTTP request (fetch/XHR) is made.

**Event data:**

```typescript theme={null}
{
  type: string                              // e.g., "fetch", "xhr"
  url: string                               // Request URL
  method: string                            // HTTP method
  headers: Record<string, string>           // Request headers
  data: Record<string, unknown>             // Request body
  response?: {
    status: string                          // Response status code
    data: Record<string, unknown>           // Response body
    headers: Record<string, string>         // Response headers
  }
}
```

**Example:**

```typescript theme={null}
await page.on('xhr', async (event) => {
  const request = event.data

  console.log(`${request.method} ${request.url}`)

  if (request.response) {
    console.log(`Response status: ${request.response.status}`)
  }
})
```

### `started`

Emitted when page load starts.

**Example:**

```typescript theme={null}
await page.on('started', async (event) => {
  console.log('Page load started')
})
```

### `finished`

Emitted when page load finishes.

**Example:**

```typescript theme={null}
await page.on('finished', async (event) => {
  console.log('Page load finished')
})
```

### `locationchange`

Emitted when the page URL changes.

**Example:**

```typescript theme={null}
await page.on('locationchange', async (event) => {
  const newUrl = await page.url()
  console.log('URL changed to:', newUrl)
})
```

### `domchange`

Emitted when the DOM is mutated.

**Example:**

```typescript theme={null}
await page.on('domchange', async (event) => {
  console.log('DOM changed')
})
```

### `dispatch`

Emitted for custom events dispatched by the page.

**Example:**

```typescript theme={null}
await page.on('dispatch', async (event) => {
  console.log('Custom event:', event.data)
})
```

### `close`

Emitted when the browser requests to close (e.g., user clicks close).

**Example:**

```typescript theme={null}
await page.on('close', async (event) => {
  console.log('Browser close requested')
  // Cleanup or save state
})
```

### `closed`

Emitted when the page is actually closed.

**Example:**

```typescript theme={null}
await page.on('closed', async (event) => {
  console.log('Page closed')
})
```

### `hostblocked`

Emitted when a host is blocked by an allowlist.

**Example:**

```typescript theme={null}
await page.on('hostblocked', async (event) => {
  console.log('Blocked host:', event.data)
})
```

## Complete examples

### Monitor all HTTP requests

```typescript theme={null}
const controller = await page.on('xhr', async (event) => {
  const req = event.data

  console.log(`[${req.method}] ${req.url}`)

  if (req.response) {
    console.log(`  Status: ${req.response.status}`)
    console.log(`  Headers:`, req.response.headers)
  }
})

await page.goto('https://example.com')

// Stop monitoring after some time
await new Promise(resolve => setTimeout(resolve, 10000))
await page.off(controller)
```

### Filter API requests

```typescript theme={null}
await page.on('xhr', async (event) => {
  const req = event.data

  // Only log API calls
  if (req.url.includes('/api/')) {
    console.log('API Request:', req.method, req.url)

    if (req.response) {
      console.log('API Response:', req.response.status)

      // Log response data for specific endpoints
      if (req.url.includes('/api/users')) {
        console.log('Users data:', req.response.data)
      }
    }
  }
})

await page.goto('https://example.com/app')
```

### Track page lifecycle

```typescript theme={null}
await page.on('started', async () => {
  console.log('[Lifecycle] Page load started')
})

await page.on('finished', async () => {
  console.log('[Lifecycle] Page load finished')
})

await page.on('locationchange', async () => {
  const url = await page.url()
  console.log('[Lifecycle] URL changed:', url)
})

await page.goto('https://example.com')
await page.click('#navigate-link')
```

### Count DOM changes

```typescript theme={null}
let domChangeCount = 0

const controller = await page.on('domchange', async () => {
  domChangeCount++
})

await page.goto('https://example.com/dynamic-page')

// Wait for page to settle
await new Promise(resolve => setTimeout(resolve, 5000))

await page.off(controller)
console.log(`Total DOM changes: ${domChangeCount}`)
```

### Wait for specific request

```typescript theme={null}
async function waitForAPICall(page, apiPath) {
  return new Promise((resolve) => {
    page.on('xhr', async (event) => {
      const req = event.data

      if (req.url.includes(apiPath) && req.response) {
        resolve(req.response)
      }
    })
  })
}

// Click button that triggers API call
await page.click('#load-data')

// Wait for the API call to complete
const response = await waitForAPICall(page, '/api/data')
console.log('API call completed:', response.status)
```

### Detect failed requests

```typescript theme={null}
const failedRequests = []

await page.on('xhr', async (event) => {
  const req = event.data

  if (req.response && parseInt(req.response.status) >= 400) {
    failedRequests.push({
      url: req.url,
      method: req.method,
      status: req.response.status
    })

    console.error(`Failed request: ${req.method} ${req.url} - ${req.response.status}`)
  }
})

await page.goto('https://example.com')

console.log(`Total failed requests: ${failedRequests.length}`)
```

### Monitor page navigation

```typescript theme={null}
const navigationHistory = []

await page.on('locationchange', async () => {
  const url = await page.url()
  navigationHistory.push({
    url,
    timestamp: Date.now()
  })

  console.log('Navigated to:', url)
})

await page.goto('https://example.com')
await page.click('#link1')
await page.click('#link2')
await page.click('#back-button')

console.log('Navigation history:')
navigationHistory.forEach((nav, i) => {
  console.log(`${i + 1}. ${nav.url} at ${new Date(nav.timestamp).toISOString()}`)
})
```

### Measure request performance

```typescript theme={null}
const requestTimes = new Map()

await page.on('xhr', async (event) => {
  const req = event.data

  if (!requestTimes.has(req.url)) {
    requestTimes.set(req.url, {
      startTime: Date.now(),
      url: req.url
    })
  }

  if (req.response) {
    const startTime = requestTimes.get(req.url)?.startTime
    if (startTime) {
      const duration = Date.now() - startTime
      console.log(`${req.url} took ${duration}ms`)
    }
  }
})

await page.goto('https://example.com')
```

### Handle custom events

```typescript theme={null}
// Add user script to dispatch custom events
await page.addUserScript(`
  window.addEventListener('load', () => {
    // Dispatch custom event
    window.dispatchEvent(new CustomEvent('app-ready', {
      detail: { version: '1.0' }
    }))
  })
`)

// Listen for custom events
await page.on('dispatch', async (event) => {
  console.log('Custom event received:', event.type, event.data)
})

await page.goto('https://example.com')
```

### Cleanup on page close

```typescript theme={null}
let eventListeners = []

async function setupListeners(page) {
  const xhrController = await page.on('xhr', xhrHandler)
  const domController = await page.on('domchange', domHandler)

  eventListeners.push(xhrController, domController)

  // Cleanup when page closes
  await page.on('closed', async () => {
    console.log('Page closed, cleaning up listeners')

    for (const controller of eventListeners) {
      await page.off(controller)
    }

    eventListeners = []
  })
}

await setupListeners(page)
```

### Debug mode logging

```typescript theme={null}
async function enableDebugMode(page) {
  await page.on('xhr', async (event) => {
    const req = event.data
    console.log('[XHR]', req.method, req.url)
  })

  await page.on('started', async () => {
    console.log('[NAV] Page load started')
  })

  await page.on('finished', async () => {
    console.log('[NAV] Page load finished')
  })

  await page.on('locationchange', async () => {
    const url = await page.url()
    console.log('[NAV] Location changed:', url)
  })

  await page.on('domchange', async () => {
    console.log('[DOM] DOM mutated')
  })

  console.log('Debug mode enabled')
}

await enableDebugMode(page)
await page.goto('https://example.com')
```

## Best practices

<AccordionGroup>
  <Accordion title="Clean up listeners" icon="broom">
    Always remove event listeners when done:

    ```typescript theme={null}
    const controller = await page.on('xhr', handler)

    // Do work

    await page.off(controller)
    ```
  </Accordion>

  <Accordion title="Use once() for one-time events" icon="1">
    Use `once()` for events you only need to handle once:

    ```typescript theme={null}
    await page.once('finished', async () => {
      console.log('Page loaded')
    })
    ```
  </Accordion>

  <Accordion title="Filter events" icon="filter">
    Filter events in your handler to avoid processing irrelevant data:

    ```typescript theme={null}
    await page.on('xhr', async (event) => {
      const req = event.data

      if (!req.url.includes('/api/')) return  // Skip non-API requests

      // Process API request
    })
    ```
  </Accordion>

  <Accordion title="Handle async handlers" icon="arrows-spin">
    Event handlers can be async, but they don't block the page:

    ```typescript theme={null}
    await page.on('xhr', async (event) => {
      // This is async but doesn't block page execution
      await processRequest(event.data)
    })
    ```
  </Accordion>
</AccordionGroup>

## Related

<CardGroup cols={2}>
  <Card title="Waiting" icon="hourglass" href="/api-reference/page/waiting">
    Wait for specific requests
  </Card>

  <Card title="Requests" icon="globe" href="/api-reference/page/requests">
    Make HTTP requests
  </Card>

  <Card title="JavaScript" icon="code" href="/api-reference/page/javascript">
    Execute JavaScript
  </Card>

  <Card title="Page overview" icon="window-maximize" href="/api-reference/page/overview">
    Back to Page API overview
  </Card>
</CardGroup>
