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

# Browser telemetry quickstart

> Install @aient/otel-browser, register it at app boot with a publishable key, and send frontend traces and logs from React or Next.js apps to Aient.

# Browser telemetry quickstart

Install the browser package and start telemetry once during application boot.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @aient/otel-browser
```

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { registerOTelBrowser } from '@aient/otel-browser'

registerOTelBrowser({
  publishableKey: import.meta.env.VITE_AIENT_PUBLISHABLE_KEY,
  serviceName: 'web',
  installationId: '0198d2f4-1111-7111-8111-111111111111', // caller-owned
  contextAttributes: { 'business.id': 'business_123' },
  release: {
    commit: import.meta.env.VITE_COMMIT_SHA,
    branch: import.meta.env.VITE_COMMIT_REF,
    environment: import.meta.env.MODE,
  },
})
```

## React and Next.js

Browsers only: call `registerOTelBrowser` from a **client** component after mount (not during render). Initialize **once** at a long-lived application boundary, then update user and business/workspace context without re-creating the SDK. Cleanup synchronously detaches instrumentation before asynchronously draining providers, so an immediate Strict Mode remount can register cleanly. The package must be the sole owner of the page's OpenTelemetry trace, context, propagation, and logs globals; registration rejects a competing owner without replacing it.

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
'use client'

import { useEffect, useRef } from 'react'
import { registerOTelBrowser, type BrowserSDK } from '@aient/otel-browser'

export function TelemetryInit({ analyticsConsent, userId, pseudoId, businessId }: { analyticsConsent: boolean; userId?: string | null; pseudoId?: string | null; businessId?: string | null }) {
  const sdkRef = useRef<BrowserSDK | null>(null)

  useEffect(() => {
    const installationKey = 'my-app.otel.installation-id'
    if (!analyticsConsent) {
      localStorage.removeItem(installationKey)
      return
    }
    const installationId = localStorage.getItem(installationKey) ?? crypto.randomUUID()
    localStorage.setItem(installationKey, installationId)

    const sdk = registerOTelBrowser({
      publishableKey: process.env.NEXT_PUBLIC_AIENT_PUBLISHABLE_KEY!,
      serviceName: process.env.NEXT_PUBLIC_OTEL_SERVICE_NAME ?? 'web',
      installationId,
      release: {
        commit: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA ?? 'dev',
        branch: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF ?? 'local',
        environment: process.env.NEXT_PUBLIC_VERCEL_ENV ?? 'development',
      },
    })
    sdkRef.current = sdk
    return () => {
      sdkRef.current = null
      void sdk.shutdown()
    }
  }, [analyticsConsent])

  useEffect(() => {
    if (userId || pseudoId) {
      sdkRef.current?.identify({ ...(userId ? { userId } : {}), ...(pseudoId ? { pseudoId } : {}) })
    } else {
      sdkRef.current?.clearUser()
    }
  }, [analyticsConsent, userId, pseudoId])

  useEffect(() => {
    sdkRef.current?.setContextAttributes(businessId ? { 'business.id': businessId } : null)
  }, [analyticsConsent, businessId])

  return null
}
```

In the App Router, render `TelemetryInit` from `app/layout.tsx` (or a client shell). Pass analytics permission from your consent manager as `analyticsConsent`, the authenticated id as `userId`, and only pass `pseudoId` when you have a distinct pseudonymous person identifier. The caller-owned installation ID is a separate application-installation lifetime and is persisted only while analytics consent permits it. For the Pages Router, mount the same component under `_app`. Use `NEXT_PUBLIC_*` only for values that are safe to expose to the browser (the publishable key is designed for that).

## Dynamic context

Use static `attributes` for resource facts that do not change during the SDK lifetime, such as deployment metadata. Supply a caller-owned `installationId` for one application installation; the SDK emits it as `app.installation.id` but does not persist it. Use `contextAttributes` and `sdk.setContextAttributes()` for mutable correlation values such as the active business, account, or workspace. `identify()` replaces the whole user snapshot and `clearUser()` removes it while preserving the installation.

Each span captures dynamic context when it starts. Updating context affects future spans and outgoing Fetch/XHR baggage, but does not mutate in-flight or ended spans.

## Click capture

The SDK owns one document-level capture listener and emits exactly one synchronous point span for each eligible DOM click event that reaches the document, including synthetic clicks. A target handler may call `preventDefault()`, return `false`, or stop propagation without suppressing the span because capture has already run. Disabled targets are excluded.

Click spans are point observations, not parents for application work triggered by the click. Fetch/XHR and other later spans keep their natural active-context parentage.

## Release metadata

Use the same `serviceName` and `release.commit` when uploading source maps. That lets Aient connect browser stack frames to the correct original source.

## Privacy

Do not attach secrets, auth tokens, raw request bodies, or user-entered sensitive content to spans or logs. Publishable keys identify only the Aient environment and are safe for browser code.

Outgoing W3C baggage may carry `app.installation.id`, `enduser.id`, and `enduser.pseudo.id` as explicitly untrusted correlation claims. Email and role stay local. Aient backends still derive authenticated identity from the verified server session and overwrite the browser claim. See the [SDK reference](../reference/sdk-browser) for details.

Dynamic context and browser baggage are untrusted correlation hints. Never use them to authorize access or replace server-side session and identity checks. Generic context keys under `enduser.*` are reserved and omitted.

## Transport

`@aient/otel-browser` sends OTLP HTTP JSON payloads. `http/protobuf` is not bundled; legacy protobuf config falls back to JSON.
