npm package: @glimt.dev/otel-browser
Description: Minimal browser-only OpenTelemetry bootstrap for Glimt.dev
Author: Glimt.dev
Latest version: 0.2.1

# @glimt.dev/otel-browser

OpenTelemetry for the browser. Works out of the box, highly configurable.

## Quick Start (3 lines)

```bash
npm install @glimt.dev/otel-browser @opentelemetry/api @opentelemetry/api-logs
```

```ts
import { registerOTelBrowser } from '@glimt.dev/otel-browser'

registerOTelBrowser({ serviceName: 'my-web-app' })
```

Done. Traces go to `https://ingest.glimt.dev/v1/traces`, logs to `/v1/logs`.

### Local Development

For local development with a local collector:

```ts
registerOTelBrowser({
  serviceName: 'my-web-app',
  exporterUrl: 'http://localhost:4318',
})
```

### Production: Glimt.dev Ingest

For production with Glimt authentication:

```ts
registerOTelBrowser({
  serviceName: 'my-web-app',
  // exporterUrl defaults to https://ingest.glimt.dev
  organisationId: 'your-org-id',
  publishableKey: 'your-publishable-key',
})
```

---

## Full Example (all options)

```ts
import { registerOTelBrowser } from '@glimt.dev/otel-browser'

const sdk = registerOTelBrowser({
  // Identity
  serviceName: 'my-web-app',

  // Glimt auth
  organisationId: 'org_xxx',
  publishableKey: 'pk_xxx',

  // Release metadata (CRITICAL for source mapping)
  release: {
    commit: process.env.NEXT_PUBLIC_COMMIT_SHA,  // or 'abc1234'
    branch: process.env.NEXT_PUBLIC_COMMIT_REF,  // or 'main'
    version: '1.0.0',
    environment: 'production',
  },

  // Export - base URL, SDK derives /v1/traces and /v1/logs
  exporterUrl: 'https://ingest.glimt.dev',
  exporter: 'http/protobuf',  // or 'http/json'
  exporterHeaders: { 'x-tenant-id': 'acme' },
  credentials: 'include',  // for CORS

  // Logs (auto-derived to /v1/logs)
  logs: {
    exporter: 'http/protobuf',
  },

  // Instrumentations
  instrumentations: ['auto'],  // document-load, user-interaction, xhr, fetch
  fetch: {
    ignoreUrls: [/\/healthz?$/, /analytics\.js$/],
    propagateContextUrls: [/^https:\/\/api\.myapp\.com/],
  },

  // Capture
  captureUnhandledErrors: true,  // window.onerror, unhandledrejection (default: true)
  captureConsoleLogs: true,      // console.* as OTLP logs (default: false)
  includeUserAgent: false,       // privacy default

  // Initial user context
  user: { userId: 'user_123', role: 'admin' },

  // Debug
  logLevel: 'DEBUG',
})

// Update user on login/logout
sdk.setUserContext({ userId: 'user_456' })
sdk.setUserContext(null)
```

---

## Environment Variables (via Build Tools)

Browsers don't have runtime environment variables, but you can inject them at build time using your bundler:

**Next.js (`next.config.mjs`):**
```js
// Automatically available as process.env.NEXT_PUBLIC_*
// Set in .env or CI environment
```

**Vite (`vite.config.ts`):**
```ts
export default defineConfig({
  define: {
    'import.meta.env.VITE_OTEL_ENDPOINT': JSON.stringify(process.env.OTEL_EXPORTER_OTLP_ENDPOINT),
    'import.meta.env.VITE_COMMIT_SHA': JSON.stringify(process.env.COMMIT_SHA),
  }
})
```

**Webpack:**
```js
new webpack.DefinePlugin({
  'process.env.OTEL_ENDPOINT': JSON.stringify(process.env.OTEL_EXPORTER_OTLP_ENDPOINT),
})
```

---

## React / Next.js Integration

```tsx
'use client'
import { useEffect, useRef } from 'react'
import { registerOTelBrowser, type BrowserSDK } from '@glimt.dev/otel-browser'

export function TelemetryProvider({ userId }: { userId?: string }) {
  const sdk = useRef<BrowserSDK | null>(null)

  useEffect(() => {
    sdk.current = registerOTelBrowser({
      serviceName: process.env.NEXT_PUBLIC_OTEL_SERVICE_NAME!,
      exporterUrl: process.env.NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT,
      user: userId ? { userId } : undefined,
      release: {
        commit: process.env.NEXT_PUBLIC_COMMIT_SHA || process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
        branch: process.env.NEXT_PUBLIC_COMMIT_REF || process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF,
        environment: process.env.NEXT_PUBLIC_VERCEL_ENV,
      }
    })
  }, [])

  useEffect(() => {
    sdk.current?.setUserContext(userId ? { userId } : null)
  }, [userId])

  return null
}
```

```tsx
// app/layout.tsx
export default async function RootLayout({ children }) {
  const user = await getCurrentUser()
  return (
    <html>
      <body>
        <TelemetryProvider userId={user?.id} />
        {children}
      </body>
    </html>
  )
}
```

---

## Server-side traceparent

Inject in your HTML to correlate browser ↔ server:

```html
<meta name="traceparent" content="00-abc123...-def456...-01" />
```

The SDK picks this up automatically as the parent span.

---

## CORS Requirements

Your collector must allow:

```
Access-Control-Allow-Origin: * (or your origin)
Access-Control-Allow-Headers: content-type, traceparent, baggage, authorization
Access-Control-Allow-Credentials: true (if using credentials)
```

---

## Defaults

| Feature | Default |
|---------|---------|
| Trace exporter | `http/protobuf` → `https://ingest.glimt.dev/v1/traces` |
| Log exporter | `http/protobuf` → `https://ingest.glimt.dev/v1/logs` |
| Instrumentations | document-load, user-interaction, xhr, fetch |
| Error capture | `window.onerror`, `unhandledrejection` |
| User agent | NOT included (privacy) |
| Console capture | OFF |

---

## User Context Attributes

When you call `sdk.setUserContext()`, these attributes are added to all spans:

| Attribute | Description |
|-----------|-------------|
| `enduser.id` | Primary user identifier |
| `enduser.pseudo.id` | Privacy-preserving hash |
| `enduser.email` | PII, use with caution |
| `enduser.role` | Permission scope |

---

## License

MIT