npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aient/otel-browser

v0.6.0

Published

Minimal browser-only OpenTelemetry bootstrap for Aient

Readme

@aient/otel-browser

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

Quick Start (3 lines)

npm install @aient/otel-browser @opentelemetry/api @opentelemetry/api-logs
import { registerOTelBrowser } from '@aient/otel-browser'

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

Done. Traces go to https://ingest.aient.ai/v1/traces, logs to /v1/logs.

Local Development

For local development with a local collector:

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

Production: Aient.dev Ingest

For production with Aient authentication:

registerOTelBrowser({
  serviceName: 'my-web-app',
  // exporterUrl defaults to https://ingest.aient.ai
  publishableKey: 'pk_live_11...',
})

Full Example (all options)

import { registerOTelBrowser } from '@aient/otel-browser'

const sdk = registerOTelBrowser({
  // Identity
  serviceName: 'my-web-app',
  // Caller-owned installation ID (persist/rotate according to your consent policy)
  installationId: '0198d2f4-1111-7111-8111-111111111111',

  // Aient auth
  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.aient.ai',
  exporter: 'http/json',
  exporterHeaders: { 'x-tenant-id': 'acme' },
  credentials: 'include',  // for CORS

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

  // Instrumentations
  instrumentations: ['auto'],  // document-load, exact click capture, 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', pseudoId: 'anon_abc', role: 'admin' },

  // Dynamic correlation context for future spans and outgoing baggage
  contextAttributes: { 'business.id': 'business_123' },

  // Debug
  logLevel: 'DEBUG',
})

// Update user on login/logout without changing the installation ID
sdk.identify({ userId: 'user_456', pseudoId: 'pseudo_abc' })
sdk.clearUser()
sdk.identify({ userId: 'user_789' })

// Replace context after a business/workspace switch; clear it on logout
sdk.setContextAttributes({ 'business.id': 'business_456' })
sdk.setContextAttributes(null)

attributes and contextAttributes have different lifecycles. Use attributes for static resource metadata such as deployment or service facts. Use contextAttributes for mutable correlation values such as the active business, account, or workspace. Each span captures the current dynamic snapshot when it starts, so updates do not retroactively change in-flight or ended spans.

Dynamic context is also propagated as W3C baggage on outgoing Fetch and XHR requests. Treat it only as telemetry correlation data: browser-provided app.installation.id, enduser.id, and enduser.pseudo.id are untrusted claims and must never authorize access or replace server-side identity/session checks. Email and role are never propagated. Keys under enduser.* and app.installation.id are SDK-owned and omitted from generic context.

http/json is the only bundled OTLP transport. Legacy http/protobuf values are ignored at runtime and fall back to JSON.

OTLP JSON protocol contract

The SDK exports traces and logs as OTLP/HTTP JSON protobuf payloads with Content-Type: application/json.

The serializer follows the OTLP JSON mapping used by the OpenTelemetry protocol:

  • request envelopes are ExportTraceServiceRequest and ExportLogsServiceRequest JSON objects (resourceSpans and resourceLogs);
  • JSON field names are lowerCamelCase, for example startTimeUnixNano and droppedAttributesCount;
  • trace IDs and span IDs are hex strings, not base64;
  • 64-bit timestamps and integer attribute values are decimal strings;
  • enum fields such as span kind, span status, and log severity are encoded as numbers;
  • arbitrary byte attributes are base64 strings;
  • spans and logs are grouped by resource and instrumentation scope, including schema URLs when present.

@aient/otel-browser does not bundle protobufjs, @opentelemetry/otlp-transformer, or @opentelemetry/otlp-exporter-base. Protocol-sensitive serializer behavior is covered by test/otlp-json-serializer.test.ts, and dependency regressions are covered by test/package-contract.test.ts.


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):

// Automatically available as process.env.NEXT_PUBLIC_*
// Set in .env or CI environment

Vite (vite.config.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:

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

React / Next.js Integration

Use a client-only component at a long-lived application boundary: registering during render can duplicate exporters. Register only in the initialization effect, and update identity in a separate effect without recreating the SDK. shutdown() synchronously detaches instrumentations and global OpenTelemetry registrations before asynchronously draining providers, so an immediate React Strict Mode remount starts a clean generation. Tracer and logger handles cached from the global OpenTelemetry APIs follow the active generation and become no-ops while no @aient/otel-browser SDK is registered.

This package is a browser telemetry bootstrap and must be the sole owner of the page's OpenTelemetry trace, context, propagation, and logs globals. Registration throws without replacing or removing a host application's existing OpenTelemetry globals.

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

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

  useEffect(() => {
    const sdk = registerOTelBrowser({
      publishableKey: process.env.NEXT_PUBLIC_AIENT_PUBLISHABLE_KEY!,
      serviceName: process.env.NEXT_PUBLIC_OTEL_SERVICE_NAME ?? 'web',
      installationId,
      ...(process.env.NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT
        ? { exporterUrl: process.env.NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT }
        : {}),
      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 () => {
      void sdk.shutdown()
    }
  }, [])

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

  return null
}
// app/layout.tsx (App Router)
import { cookies } from 'next/headers'

export default async function RootLayout({ children }) {
  const user = await getCurrentUser()
  const installationId = (await cookies()).get('app_installation_id')?.value
  const pseudonymousUserId = user
    ? (await cookies()).get('pseudonymous_user_id')?.value
    : undefined
  return (
    <html>
      <body>
        <TelemetryInit userId={user?.id} pseudoId={pseudonymousUserId} installationId={installationId} />
        {children}
      </body>
    </html>
  )
}

Browser ↔ server trace context

The SDK does not read a traceparent from HTML metadata. Browser/server continuity is established when the configured Fetch/XHR instrumentation injects W3C trace-context headers from an active browser span into an allowed request.


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/jsonhttps://ingest.aient.ai/v1/traces | | Log exporter | http/jsonhttps://ingest.aient.ai/v1/logs | | Instrumentations | document-load, one document-capture click span per event, xhr, fetch | | Error capture | window.onerror, unhandledrejection | | User agent | NOT included (privacy) | | Console capture | OFF |


Identity lifecycle

installationId represents one application installation and is emitted as the app.installation.id resource attribute. The generic SDK does not create or persist it. identify() replaces the complete user portion; clearUser() removes it while retaining the installation. getIdentity() returns a clone of the composite snapshot.

sdk.getIdentity() // { installationId: '...' }
sdk.identify({ userId: authenticatedUser.id })
sdk.getIdentity() // { installationId: '...', userId: '...' }
sdk.clearUser()
sdk.getIdentity() // { installationId: '...' }

User fields are added to future spans and logs:

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

setUserContext() and getUserContext() remain compatibility aliases over the same generation-owned user snapshot. Identity calls do not mutate ambient OpenTelemetry context or baggage. The span processor reads at span start, the log processor at emit, and the baggage propagator at request injection.

sdk.identify({ userId: authenticatedUser.id, pseudoId: pseudonymousUserId })

The SDK emits both attributes but does not emit an identity-link event. Link events are application-domain telemetry and must define their own privacy, deduplication, and payload contract.

Lifecycle and version capabilities

| Version | Relevant capability | |---------|---------------------| | 0.4.1 | User context (setUserContext), but no dynamic context API | | 0.5.0 | Adds setContextAttributes and getContextAttributes | | 0.5.1 | Complete instrumentation/global teardown, safe immediate re-registration, blank-error normalization, and ResizeObserver loop filtering | | 0.6.0 | Exact one-event/one-click capture plus flat identify, clearUser, getIdentity, installation correlation, and untrusted user baggage |

If a runtime method is missing, inspect the installed artifact rather than the workspace manifest:

npm ls @aient/otel-browser

Then verify the resolved package in the lockfile and reinstall/rebuild the consuming app. An already-loaded older browser bundle cannot diagnose capabilities introduced by a newer package.

Dynamic Context Attributes

Call sdk.setContextAttributes() when mutable application context changes:

sdk.setContextAttributes({
  'business.id': 'business_123',
  'workspace.id': 'workspace_456',
})

const snapshot = sdk.getContextAttributes()
sdk.setContextAttributes(null)

The setter replaces the whole snapshot rather than merging it. Values are cloned on set and get. Future spans receive the snapshot at span start, while existing spans remain unchanged. Outgoing baggage preserves unrelated entries and replaces SDK-owned dynamic keys.

Click capture

The auto and explicit user selectors install one document capture listener per SDK generation. Every eligible DOM click reaching it creates one synchronous point span named click, even when target handlers later call preventDefault(), return false, or stop propagation. The SDK does not patch application listeners, debounce events, cache fingerprints, or deduplicate exports. Because the span ends at the capture boundary, it is not an active parent around later application handlers.


License

MIT