---
title: "Client Logging"
description: "Capture browser events with structured logging. Same API as the server, with automatic console styling, user identity context, and optional server transport."
canonical_url: "https://www.evlog.dev/use-cases/client-logging"
---
# Client Logging

> Capture browser events with structured logging. Same API as the server, with automatic console styling, user identity context, and optional server transport.

Server logs tell you what happened on the backend. Client logs complete the picture: user interactions, page views, frontend errors, and performance signals that never reach the server unless you capture them.

<client-server-beacon>



</client-server-beacon>

<prompt :actions="["copy","cursor","claude"]" description="Ship browser logs to my server" icon="i-lucide-monitor">

Ship browser logs to my server with evlog client logging.

- Detect my framework (Nuxt, Next.js, SvelteKit, etc.) and pick the matching client entrypoint
- Call initLog({ service: 'web' }) once at app start
- Use log.info / log.warn / log.error in components, composables, and event handlers
- Set user identity with setIdentity({ userId, email }) after login, clearIdentity() on logout
- Enable transport in the framework config so logs POST to /api/_evlog/ingest in batches
- On the server, evlog auto-runs the drain pipeline on transported events with source: 'client'

Docs: [https://www.evlog.dev/use-cases/client-logging](https://www.evlog.dev/use-cases/client-logging)
HTTP transport: [https://www.evlog.dev/extend/drain-pipeline](https://www.evlog.dev/extend/drain-pipeline)

</prompt>

## Quick Start

evlog provides a client-side logging API that works in any browser environment:

<framework-tabs>

```typescript [Nuxt]app/plugins/logger.client.ts
import { initLog, log } from 'evlog/client'

export default defineNuxtPlugin(() => {
  initLog({ service: 'web' })

  log.info({ action: 'app_init', path: window.location.pathname })
})
```

```typescript [app/providers.tsx (React / Next.js)]
'use client'
import { useEffect } from 'react'
import { initLog, log } from 'evlog/client'

export function LogProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    initLog({ service: 'web' })
    log.info({ action: 'app_init', path: window.location.pathname })
  }, [])

  return <>{children}</>
}
```

```typescript [src/app.ts (Any frontend)]
import { initLog, log } from 'evlog/client'

initLog({ service: 'web' })
log.info({ action: 'app_init', path: window.location.pathname })
```

</framework-tabs>

The `log` object works anywhere in your client code: components, composables, event handlers.

## Minimum level (`minLevel`)

Use `initLog({ minLevel: 'warn' })` to keep the browser console quiet (warnings and errors only). Severity order: `trace` < `debug` < `info` < `warn` < `error` < `fatal`. Default is `'debug'` (all levels).

For a **debug toggle** without reloading, call `setMinLevel('debug')` or `setMinLevel('warn')` from `evlog/client` when the user opts in or out of verbose logs.

`minLevel` applies to both console output and [server transport](#sending-logs-to-the-server) payloads.

## Call it two ways

The `log` API accepts two forms depending on the context.

### Object Form (structured context)

Pass an object to capture structured data, just like server-side `log.set()`:

```typescript [pages/products.vue]
log.info({ action: 'page_view', path: '/products', referrer: document.referrer })
```

```bash [Browser console]
[web] info  { action: 'page_view', path: '/products', referrer: 'https://google.com' }
```

### Tag + Message Form (quick logs)

Pass a tag and a message for quick, readable logs:

```typescript [composables/useAuth.ts]
log.info('auth', 'User logged in')
```

```bash [Browser console]
[auth] User logged in
```

The message accepts printf-style interpolation, the same [formatting as server-side logs](https://www.evlog.dev/learn/simple-logging#interpolate-values-into-the-message). When it contains `%s`, `%d`, or `%j` and extra arguments follow, evlog formats it before writing to the console:

```typescript [composables/useCart.ts]
log.info('cart', 'Item %s added, cart total %d', 'SKU-42', 9999)
```

```bash [Browser console]
[cart] Item SKU-42 added, cart total 9999
```

### Levels you can use

Both forms support six levels: `log.fatal()`, `log.info()`, `log.warn()`, `log.error()`, `log.debug()`, and `log.trace()`.

In the browser, `log.debug()` is emitted with `console.log` (not `console.debug`) so lines stay visible with the default DevTools **Info** filter; the structured event still has `level: 'debug'`.

## Attach who the log belongs to

Track which user generated a log with `setIdentity()`:

```typescript [composables/useAuth.ts]
import { setIdentity, clearIdentity, log } from 'evlog/client'

// After login
setIdentity({ userId: 'usr_123', plan: 'pro' })

log.info({ action: 'dashboard_view' })
// → { userId: 'usr_123', plan: 'pro', action: 'dashboard_view', ... }

// After logout
clearIdentity()
```

Identity fields are automatically merged into every log event until cleared. This lets you correlate browser events to specific users in your observability tools.

## Configuration

`initLog()` accepts the following options:

<table>
<thead>
  <tr>
    <th>
      Option
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        enabled
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Enable or disable all client logging
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        console
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Output logs to the browser console
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        pretty
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Use colored, formatted console output
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        minLevel
      </code>
    </td>
    
    <td>
      <code>
        'debug'
      </code>
    </td>
    
    <td>
      Minimum severity: <code>
        trace
      </code>
      
       < <code>
        debug
      </code>
      
       < <code>
        info
      </code>
      
       < <code>
        warn
      </code>
      
       < <code>
        error
      </code>
      
       < <code>
        fatal
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        service
      </code>
    </td>
    
    <td>
      <code>
        'client'
      </code>
    </td>
    
    <td>
      Service name included in every log event
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        transport
      </code>
    </td>
    
    <td>
      -
    </td>
    
    <td>
      Send logs to a server endpoint (see below)
    </td>
  </tr>
</tbody>
</table>

```typescript [app/plugins/logger.client.ts]
initLog({
  service: 'web',
  transport: {
    enabled: true,
    endpoint: '/api/_evlog/ingest', // default endpoint
  },
})
```

<callout color="info" icon="i-lucide-info">

`enabled`, `console`, and `pretty` all default to `true`. You only need to set them if you want to change the defaults.

</callout>

## Sending Logs to the Server

By default, client logs only appear in the browser console. To persist them, you have two options:

### Ship logs with the built-in drain

The simplest approach is to enable the built-in transport in `initLog()`. Each log is sent individually via `fetch` with `keepalive: true`. Good for low-volume apps.

<framework-tabs>

```typescript [Nuxt]app/plugins/logger.client.ts
import { initLog } from 'evlog/client'

export default defineNuxtPlugin(() => {
  initLog({
    service: 'web',
    transport: {
      enabled: true,
      endpoint: '/api/_evlog/ingest',
    },
  })
})
```

```tsx [Next.js]app/layout.tsx
import { EvlogProvider } from 'evlog/next/client'

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <EvlogProvider service="web" transport={{ enabled: true, endpoint: '/api/_evlog/ingest' }}>
          {children}
        </EvlogProvider>
      </body>
    </html>
  )
}
```

```typescript [src/app.ts (Any frontend)]
import { initLog } from 'evlog/client'

initLog({
  service: 'web',
  transport: {
    enabled: true,
    endpoint: '/api/_evlog/ingest',
  },
})
```

</framework-tabs>

<callout color="info" icon="i-lucide-info">

In Nuxt with the evlog module, the server ingest endpoint is auto-registered. For other frameworks, you need to create the endpoint yourself. See the [HTTP drain](https://www.evlog.dev/extend/drain-pipeline#server-endpoint) docs for Express and Hono examples.

</callout>

### HTTP drain pipeline

For higher volume or when you need batching, retries, and page-exit flushing, use the HTTP drain (`evlog/http`). This works with any frontend and has no framework dependency.

<framework-tabs>

```typescript [Nuxt]app/plugins/logger.client.ts
import { initLogger, log } from 'evlog'
import { createHttpLogDrain } from 'evlog/http'

export default defineNuxtPlugin(() => {
  const drain = createHttpLogDrain({
    drain: { endpoint: '/api/_evlog/ingest' },
    pipeline: {
      batch: { size: 25, intervalMs: 2000 },
      retry: { maxAttempts: 2 },
    },
  })

  initLogger({ drain })
  log.info({ action: 'app_init' })
})
```

```typescript [src/app.ts (Any frontend)]
import { initLogger, log } from 'evlog'
import { createHttpLogDrain } from 'evlog/http'

const drain = createHttpLogDrain({
  drain: { endpoint: 'https://logs.example.com/v1/ingest' },
  pipeline: {
    batch: { size: 25, intervalMs: 2000 },
    retry: { maxAttempts: 2 },
  },
})

initLogger({ drain })
log.info({ action: 'app_init' })
```

</framework-tabs>

The HTTP drain automatically:

- **Batches** events by size and time interval
- **Retries** failed sends with exponential backoff
- **Flushes** buffered events via `sendBeacon` when the page becomes hidden (tab switch, navigation, close)

<callout color="neutral" icon="i-lucide-arrow-right">

See the [HTTP drain](https://www.evlog.dev/extend/drain-pipeline) adapter docs for full configuration reference, authentication, and server endpoint examples.

</callout>

## Next Steps

- [HTTP drain](https://www.evlog.dev/extend/drain-pipeline) - Batching, retry, and sendBeacon fallback
- [Pipeline](https://www.evlog.dev/extend/drain-pipeline) - Advanced pipeline configuration
- [Structured Errors](https://www.evlog.dev/learn/structured-errors) - Surface client errors with actionable context

---

- [HTTP drain](https://www.evlog.dev/extend/drain-pipeline)
- [Wide Events](https://www.evlog.dev/learn/wide-events)


## Sitemap

See the full [sitemap](https://www.evlog.dev/sitemap.md) for all pages.
