Documentation

SDKs

Browser

Capture frontend errors, logs, network calls, and analytics, and connect them to your backend traces

The browser SDK reports errors, logs, and fetch and XHR calls from your web application. It can also record page views and custom events. Requests to your own API carry a trace header, so a click in the browser and the backend work it triggered appear in the same trace.

The SDK works with any framework, ships its TypeScript types, and has no runtime dependencies. It targets browsers with ES2022 support.

Installation

  1. Create the DSN

    In Monocle, open Services, select your frontend service (or add one), and pick Browser under SDK connection. Copy the DSN.

  2. Install the package
    Terminal
    npm install @monocle.sh/browser-sdk
  3. Initialize the SDK

    Call initMonocle() once, in the entry point of your app, and export the client so the rest of your code can use it:

    src/monocle.ts
    import { initMonocle } from '@monocle.sh/browser-sdk'
    
    export const monocle = initMonocle({
      dsn: 'https://mk_live_xxxxxxxx@ingest.monocle.sh/00000000-0000-4000-8000-000000000001',
      environment: 'production',
    })

With no other option, the SDK records fetch and XHR calls, uncaught errors, and unhandled promise rejections.

The browser DSN is public: it ends up in your JavaScript bundle. To prevent other websites from using it, set website restrictions on the service. Never put a project API key in browser code.

You can import the SDK in code that also runs during server rendering. Called on the server, initMonocle() returns an inactive client that sends nothing.

Report the version of each build

Monocle records a deployment each time a new service.version shows up. A browser app has no environment variables at runtime, so the version has to come from your build. With Vite, add @monocle.sh/vite-plugin:

Terminal
npm install -D @monocle.sh/vite-plugin
vite.config.ts
import { defineConfig } from 'vite'
import { monocle } from '@monocle.sh/vite-plugin'

export default defineConfig({
  plugins: [monocle()],
})

The plugin detects the version during vite build and vite dev, in the same order as the server agents: MONOCLE_VERSION, platform variables, CI variables, then the commit of the local .git directory. It injects the version into the modules that import the SDK, and initMonocle() reads it. The position of monocle() in plugins doesn't matter, and the plugin makes no network calls.

OptionDescription
releaseA version to use instead of detection. false turns detection and injection off.
injectSet to false to keep the detected version out of the bundle. Defaults to true.
enabledSet to false to turn the plugin off. Defaults to true.
verboseSet to true to log the detected version and its source. Defaults to false.

A serviceVersion passed to initMonocle() wins over the injected version, unless it is a placeholder such as 0.0.0 or dev. Without Vite, pass serviceVersion yourself, from a value your bundler replaces at build time.

Options

OptionDescription
dsnRequired. DSN of the service.
environmentRequired. production, staging, or development.
serviceVersionVersion of the deployed code. Defaults to the version injected by the Vite plugin.
integrationsWhat to capture automatically. Defaults to [network(), errors()].
beforeSendLogCalled with each log before it is sent. Return the record, a modified copy, or null to drop it.

Choose what to capture

Integrations decide what the SDK captures without any code from you:

IntegrationCapturesDefault
network()fetch and XHR calls as spans, with method, URL, and status.On
errors()Uncaught errors and unhandled promise rejections.On
consoleLogs()console.debug, info, log, warn, and error calls, as logs.Off
analytics()Page views, and custom events sent with track(). See analytics.Off
TypeScript
import { analytics, consoleLogs, errors, initMonocle, network } from '@monocle.sh/browser-sdk'

export const monocle = initMonocle({
  dsn,
  environment: 'production',
  integrations: [network(), errors(), consoleLogs({ levels: ['warn', 'error'] }), analytics()],
})

A list you pass replaces the defaults, so keep network() and errors() in it if you still want them. Each integration can appear only once. To report only through the client methods, pass integrations: [].

consoleLogs() captures every console level by default. Use levels to keep fewer. The original console methods still run, and the SDK restores them on shutdown.

Report errors

Capture the errors your code handles with captureException(). Add tags to filter on and extra for any other context:

TypeScript
try {
  await checkout()
} catch (error) {
  await monocle.captureException(error, { tags: { step: 'payment' }, extra: { cartId } })
}

Report a problem that is not an Error with captureMessage(). The second argument is either a level or a context object:

TypeScript
await monocle.captureMessage('Payment form abandoned', 'warning')
await monocle.captureMessage('Payment provider unavailable', {
  level: 'error',
  tags: { provider: 'stripe' },
})

Message levels are debug, info, log, warning, error, and fatal.

Both methods return a promise that resolves once the event is queued, not once Monocle receives it. They never throw. The SDK reports a given Error object once, even if you capture it again.

The context object also accepts user, to override the current user for this event, and traceContext: { traceId, spanId }, to attach the event to a trace your code already knows.

Send logs

TypeScript
monocle.logger.info('Checkout started', { items: 3 })
monocle.logger.warn('Payment retry', { attempt: 2 })
monocle.logger.error('Payment failed', { provider: 'stripe' })

The levels are trace, debug, info, warn, error, and fatal. Attribute values are strings, numbers, or booleans. logger sends logs to Monocle without writing to the browser console.

beforeSendLog runs on every log, from logger and from consoleLogs(). Use it to drop logs or to redact them:

TypeScript
initMonocle({
  dsn,
  environment: 'production',
  beforeSendLog: (record) => {
    if (record.source === 'console' && record.level === 'debug') return null

    return record
  },
})

beforeSendLog does not apply to captureException() or captureMessage().

Identify the user

TypeScript
monocle.setUser({ id: user.id, email: user.email, name: user.name })

// On logout
monocle.setUser(null)

The user is attached to the errors, logs, and requests captured after the call. Events already queued keep the user they were captured with. The SDK keeps the user in memory only, so call setUser() again after a page reload.

Analytics events never include the user.

Track page views and events

With analytics() installed, the SDK records a page view for the first page and for each path change, including back and forward navigation. Single-page apps need no router hook. A change to the query string alone is not a new page. Hash routes such as /#/cart all count as the same page.

Record a custom event with track():

TypeScript
monocle.track({
  name: 'plan_selected',
  properties: { plan: 'pro', seats: 3, annual: true },
})

The SDK drops the whole event, without an error, if it breaks one of these rules:

  • The name is at most 100 bytes. pageview is reserved.
  • There are at most 20 properties.
  • Property keys are at most 64 bytes. Identity keys such as email, userId, ip, or session_id are rejected. So are keys that start with user, trace, span, or monocle followed by ., _, or -.
  • Property values are strings of at most 256 bytes, finite numbers, or booleans.

Keep personal information out of event names and properties.

Record page views from your router

To decide yourself when a page view counts, turn off automatic page views and call pageview() on the first page and after each completed navigation:

TypeScript
integrations: [network(), errors(), analytics({ autoPageviews: false })]
TypeScript
router.afterEach(() => monocle.pageview())

Leave autoPageviews off in this mode, or each page is counted twice.

Group pages by route

Monocle groups page views by route, such as /blog/:slug, and keeps each real URL, so a route unfolds into the pages behind it. Without any setup, Monocle builds the route by replacing segments that look like identifiers: numbers, UUIDs, hashes, and long tokens. Readable slugs stay as they are.

To use your router's own templates, pass route. Return null when a path has none:

TypeScript
// TanStack Router
analytics({ route: (path) => router.getMatchedRoutes(path)[2]?.fullPath ?? null })

// Vue Router
analytics({ route: (path) => router.resolve(path).matched.at(-1)?.path ?? null })

Any parameter syntax works: :id, $id, [id], or {id}.

Hide sensitive path segments

Use normalizePath to replace sensitive segments before they leave the browser. Unlike route, the original path is not kept. Return null to leave a path out of analytics:

TypeScript
analytics({
  normalizePath: (path) => {
    if (path.startsWith('/admin')) return null

    return path.replace(/\/orders\/[^/]+$/, '/orders/:id')
  },
})

Campaigns

The SDK sends the utm_source, utm_medium, utm_campaign, utm_term, and utm_content parameters of the landing URL with every page view and event of that page load. Monocle uses them to attribute the visit to its campaign. The SDK drops a UTM value that is longer than 128 bytes or that looks like an email address, a phone number, or an IP address.

Connect frontend and backend traces

The network() integration adds a traceparent header to requests sent to your own origin. Your backend, if instrumented with OpenTelemetry, continues the same trace.

If your API is on another origin, list it in tracePropagationTargets, and allow the traceparent request header in your API's CORS configuration:

TypeScript
network({ tracePropagationTargets: ['https://api.example.com'] })

tracePropagationTargets replaces the default, so add your own origin to the list if you still need it. To stop sending the header while still recording requests, pass tracePropagationTargets: [].

Flush and shut down

The SDK sends events in the background, in batches. When the tab is hidden, it tries to send what is queued. To send pending events right away, or to stop the client in tests or on teardown, call:

TypeScript
await monocle.flush()
await monocle.shutdown()

shutdown() stops capture, removes the SDK's patches on fetch, XHR, and the console, and sends what is left. Shut down a client before you initialize another one.

Delivery is best effort. Events still queued when the tab closes can be lost, and nothing is kept across page reloads.

Write your own integration

An integration is an object with a unique name and a synchronous setup() that returns the collectors to run. The SDK calls start() on each collector when the client starts and stop() when it shuts down. Use client to send logs, errors, and messages:

TypeScript
import type { BrowserIntegration } from '@monocle.sh/browser-sdk/types'

export function visibility(): BrowserIntegration {
  return {
    name: 'visibility',
    setup: ({ client }) => {
      const onChange = () =>
        client.logger.info('Visibility changed', { state: document.visibilityState })

      return {
        instrumentations: [
          {
            start: () => document.addEventListener('visibilitychange', onChange),
            stop: () => document.removeEventListener('visibilitychange', onChange),
          },
        ],
      }
    },
  }
}

Add it to integrations next to the built-in ones.

The public types live in two entry points:

TypeScript
import type {
  BrowserClient,
  BrowserIntegration,
  BrowserOptions,
} from '@monocle.sh/browser-sdk/types'
import type { AnalyticsEvent, AnalyticsProperties } from '@monocle.sh/browser-sdk/analytics/types'

Migrate from an API key

Configurations with endpoint, apiKey, and serviceName still work. To move to a DSN, create one for the service and replace these three options with dsn. Existing keys keep working until you revoke them. See the DSN migration guide.

Privacy

The SDK never captures request or response bodies, headers, cookies, or page content. URLs are sent without credentials, query string, or hash. analytics() uses no cookies or browser storage and doesn't send the user's identity.

Analytics keeps the landing URL's utm_source, utm_medium, utm_campaign, utm_term, and utm_content for campaign attribution. The collector drops campaign values that look like an email address, phone number, or IP address.

URL paths, error messages, stack traces, and the data you pass to the SDK can still contain personal information. Check what you send, and use normalizePath and beforeSendLog to remove it.

Not included

The SDK does not record sessions, Web Vitals, or custom spans.

Search documentation

Search pages and sections of the Monocle documentation