OneApp Docs
PackagesAI

@repo/observability

Logging, error tracking, and performance monitoring across client, server, and edge runtimes. Track bugs in production, monitor API latency, capture Web Vitals, and trace AI operations. Works with Sentry, BetterStack, and Console.

Quick Start

Add observability in 5 minutes:

pnpm add @repo/observability

Log errors, track performance, monitor Web Vitals automatically. Skip to Quick Start →

Why @repo/observability?

Production bugs are invisible without logging. You deploy a feature, users report errors, but you have no stack traces, no context, no breadcrumbs. API endpoints are slow but you don't know which database query is the bottleneck. Web Vitals are failing but you don't know which component causes layout shift.

@repo/observability solves this with unified logging across all runtimes, automatic error tracking, and performance monitoring.

Production-ready with Sentry integration, BetterStack logging, Web Vitals tracking, AI operation tracing, and distributed tracing.

Use cases

  • Error tracking — Capture production errors with stack traces, breadcrumbs, and user context
  • API monitoring — Track endpoint latency, database queries, and external API calls
  • Web performance — Monitor Core Web Vitals (LCP, FID, CLS) and identify slow components
  • AI observability — Track token usage, model latency, and costs across AI operations
  • User debugging — See exactly what users did before hitting an error

How it works

@repo/shared provides isomorphic logging, and @repo/observability handles error tracking:

import { logInfo, logError } from "@repo/shared";
import { getObservability } from "@repo/observability/server/next";

// Log with automatic context
logInfo("User checkout", {
  userId: "user_123",
  total: 99.99
});

// Errors automatically capture stack traces
try {
  await processPayment();
} catch (error) {
  logError(error, { step: "payment" });
  const observability = await getObservability();
  observability.captureException(error as Error, { step: "payment" });
  // Sent to Sentry with full context
}

Uses Sentry for error tracking, BetterStack for log aggregation, and supports client/server/edge runtimes.

Key features

Multi-runtime support — Client components, server components, edge middleware, API routes

Error tracking — Automatic stack traces, breadcrumbs, user context with Sentry

Performance monitoring — Web Vitals, API latency, distributed tracing

AI operation tracing — Track token usage, latency, and costs for AI models

Flexible providers — Sentry, BetterStack, Console, or custom plugins

Type-safe logging — TypeScript-first with runtime validation

Quick Start

1. Install the package

pnpm add @repo/observability

2. Configure Sentry (optional)

.env.local
NEXT_PUBLIC_SENTRY_DSN=https://xxx@sentry.io/xxx
SENTRY_ORG=your-org
SENTRY_PROJECT=your-project

3. Add logging to a client component

app/components/UserProfile.tsx
"use client";

import { logInfo, logError } from "@repo/observability/client/next";

export function UserProfile() {
  const handleClick = async () => {
    try {
      await logInfo("User profile viewed", { userId: "123" });
      // Your logic here
    } catch (error) {
      await logError(error as Error, { component: "UserProfile" });
    }
  };

  return <button onClick={handleClick}>View Profile</button>;
}

4. Add logging to a server component

app/page.tsx
import { logInfo } from "@repo/shared";

export default async function ServerPage() {
  logInfo("Server page rendered", {
    timestamp: new Date().toISOString()
  });

  const data = await fetchData();
  return <div>{ data };
}

That's it! Errors are automatically captured to Sentry, and logs are sent to BetterStack.

Web Vitals monitoring

Track Core Web Vitals automatically:

app/layout.tsx
import { WebVitalsReporter } from "@repo/observability/client/next";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <WebVitalsReporter />
        {children}
      </body>
    </html>
  );
}

Technical Details

For Developers: Technical implementation details

Overview

PropertyValue
Locationpackages/observability
Version2025.11.1201
Dependencies@sentry/nextjs, @logtape/logtape, PostHog
IntegrationsSentry, BetterStack, Console, LogTape
BuildTypeScript library (no build step required)

Export Paths

PathDescription
@repo/observabilityShared utilities and types
@repo/observability/clientBrowser (non-Next.js)
@repo/observability/serverNode.js server
@repo/observability/client/nextNext.js client components
@repo/observability/server/nextNext.js server components
@repo/observability/server/edgeEdge runtime (middleware)
@repo/observability/envEnvironment configuration
@repo/observability/plugins/*Provider-specific plugins

Plugin Exports

PathDescription
@repo/observability/plugins/consoleConsole logging plugin
@repo/observability/plugins/sentrySentry error tracking
@repo/observability/plugins/betterstackBetterStack logging
@repo/observability/plugins/logtapeLogTape integration
@repo/observability/plugins/sentry-microfrontendMicro-frontend Sentry

Core Features

Logging Levels

import { logDebug, logInfo, logWarn, logError } from "@repo/shared";
import { getObservability } from "@repo/observability/server/next";

// highlight-start
// Debug: Detailed diagnostic information
logDebug("Processing started", { step: 1 });

// Info: General informational messages
logInfo("User logged in", { userId: "123" });

// Warn: Warning messages for potentially harmful situations
logWarn("Rate limit approaching", { count: 95 });

// Error: Error events that might still allow the application to continue
logError(error, { context: "payment-processing" });
const observability = await getObservability();
observability.captureException(error as Error, { context: "payment-processing" });

// Fatal: Severe errors - use logError with high severity context
logError(error, { severity: "critical" });
observability.captureException(error as Error, { severity: "critical" });
// highlight-end

Error Tracking

import { captureException, captureMessage } from "@repo/observability/client/next";

try {
  await riskyOperation();
} catch (error) {
  // highlight-start
  await captureException(error as Error, {
    tags: { feature: "checkout", environment: "production" },
    extra: { userId: "user_123", cartTotal: 99.99 },
    level: "error"
  });
  // highlight-end
}

// Log custom messages
await captureMessage("Payment method expired", {
  level: "warning",
  tags: { feature: "billing" }
});

Performance Monitoring

import { startTransaction, captureMetric } from "@repo/observability/server/next";

// Distributed tracing
const transaction = startTransaction({
  // highlight-next-line
  name: "api-request",
  op: "http.server",
  tags: { endpoint: "/api/users" }
});

try {
  const data = await fetchData();
  transaction.setHttpStatus(200);
  return data;
} catch (error) {
  transaction.setHttpStatus(500);
  throw error;
} finally {
  transaction.finish();
}

// Custom metrics
await captureMetric({
  name: "cart.checkout.duration",
  value: 1234,
  unit: "millisecond",
  tags: { payment_method: "credit_card" }
});

AI Operation Tracing

AI Observability

Track AI operations to understand latency, token usage, and costs across different models.

import { traceAIOperation } from "@repo/observability/server/next";
import { generateText } from "ai";

const result = await traceAIOperation(
  {
    // highlight-start
    name: "chat-completion",
    model: "claude-3-5-sonnet",
    input: messages
    // highlight-end
  },
  async (span) => {
    const response = await generateText({ model, messages });

    // highlight-start
    span.setAttributes({
      "ai.tokens.input": response.usage.promptTokens,
      "ai.tokens.output": response.usage.completionTokens,
      "ai.cost.total": calculateCost(response.usage)
    });
    // highlight-end

    return response;
  }
);

Web Vitals Monitoring

"use client";

import { useReportWebVitals } from "next/web-vitals";
import { captureMetric } from "@repo/observability/client/next";

export function WebVitalsReporter() {
  useReportWebVitals((metric) => {
    // highlight-start
    captureMetric({
      name: `web_vital.${metric.name}`,
      value: metric.value,
      unit: "millisecond",
      tags: {
        page: window.location.pathname,
        rating: metric.rating
      }
    });
    // highlight-end
  });

  return null;
}

Core Web Vitals

MetricDescriptionGoodNeeds ImprovementPoor
LCPLargest Contentful Paint≤2.5s2.5s-4s>4s
FIDFirst Input Delay≤100ms100ms-300ms>300ms
CLSCumulative Layout Shift≤0.10.1-0.25>0.25
FCPFirst Contentful Paint≤1.8s1.8s-3s>3s
TTFBTime to First Byte≤800ms800ms-1800ms>1800ms
INPInteraction to Next Paint (NEW 2024)≤200ms200ms-500ms>500ms

Provider Configuration

Sentry Integration

// sentry.client.config.ts
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  // highlight-start
  tracesSampleRate: 1.0,
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
  // highlight-end
  integrations: [
    Sentry.replayIntegration({
      maskAllText: false,
      blockAllMedia: false
    }),
    Sentry.browserTracingIntegration()
  ]
});

BetterStack Integration

import { createBetterStackPlugin } from "@repo/observability/plugins/betterstack";

const betterStackPlugin = createBetterStackPlugin({
  sourceToken: process.env.BETTERSTACK_SOURCE_TOKEN,
  // highlight-start
  level: "info",
  includeMetadata: true,
  batchSize: 10,
  flushInterval: 5000
  // highlight-end
});

Custom Logger Configuration

import { observability } from "@repo/observability/server/next";

// Configure default plugins
observability.configure({
  plugins: [
    {
      name: "console",
      enabled: process.env.NODE_ENV === "development"
    },
    {
      name: "sentry",
      enabled: !!process.env.SENTRY_DSN,
      options: {
        tracesSampleRate: 0.1,
        profilesSampleRate: 0.1
      }
    },
    {
      name: "betterstack",
      enabled: !!process.env.BETTERSTACK_SOURCE_TOKEN
    }
  ],
  // highlight-start
  defaultTags: {
    environment: process.env.NODE_ENV,
    version: process.env.NEXT_PUBLIC_APP_VERSION
  }
  // highlight-end
});

Track user actions leading up to errors:

import { addBreadcrumb } from "@repo/observability/client/next";

// Navigation breadcrumb
addBreadcrumb({
  // highlight-next-line
  category: "navigation",
  message: "User navigated to /products",
  level: "info",
  data: { from: "/home", to: "/products" }
});

// User action breadcrumb
addBreadcrumb({
  // highlight-next-line
  category: "user",
  message: "Added item to cart",
  level: "info",
  data: { productId: "123", quantity: 2 }
});

// API call breadcrumb
addBreadcrumb({
  // highlight-next-line
  category: "http",
  message: "API call to /api/checkout",
  level: "info",
  data: { method: "POST", status: 200 }
});

Context and Tags

Global Context

import { setContext, setTags, setUser } from "@repo/observability/client/next";

// Set user context
setUser({
  id: "user_123",
  email: "user@example.com",
  username: "johndoe"
});

// Set custom context
setContext("organization", {
  id: "org_456",
  name: "Acme Corp",
  plan: "enterprise"
});

// Set tags for filtering
setTags({
  environment: "production",
  region: "us-west-2",
  feature: "checkout"
});

Scoped Context

import { withScope } from "@repo/observability/server/next";

// highlight-start
await withScope(async (scope) => {
  scope.setTag("request_id", requestId);
  scope.setContext("request", { method: "POST", path: "/api/users" });

  try {
    await processRequest();
  } catch (error) {
    scope.captureException(error);
  }
});
// highlight-end

Environment Variables

# Sentry Configuration
NEXT_PUBLIC_SENTRY_DSN=https://...@sentry.io/...
SENTRY_ORG=your-org
SENTRY_PROJECT=your-project
SENTRY_AUTH_TOKEN=...

# BetterStack Configuration
BETTERSTACK_SOURCE_TOKEN=...

# PostHog Configuration (for analytics)
NEXT_PUBLIC_POSTHOG_KEY=phc_...
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com

# General
NODE_ENV=production
NEXT_PUBLIC_APP_VERSION=1.0.0

Runtime Selection

Critical

Always use the correct runtime export to avoid errors and performance issues.

// ✅ CORRECT: Next.js Server Component
import { observability } from "@repo/observability/server/next";

// ✅ CORRECT: Next.js Client Component
import { logInfo, logError } from "@repo/observability/client/next";

// ✅ CORRECT: Edge Runtime (Middleware, Edge API Routes)
import { observability } from "@repo/observability/server/edge";

// ✅ CORRECT: Generic Node.js Server
import { observability } from "@repo/observability/server";

// ❌ NEVER: Use server/next in edge runtime
// ❌ NEVER: Use edge exports in Node.js server
// ❌ NEVER: Use client exports in server components

Advanced Patterns

Distributed Tracing

import { startTransaction, startSpan } from "@repo/observability/server/next";

const transaction = startTransaction({
  name: "checkout-flow",
  op: "transaction"
});

try {
  // Span 1: Validate cart
  // highlight-start
  const validateSpan = startSpan({
    name: "validate-cart",
    op: "validation",
    parentSpanId: transaction.spanId
  });
  await validateCart();
  validateSpan.finish();
  // highlight-end

  // Span 2: Process payment
  const paymentSpan = startSpan({
    name: "process-payment",
    op: "payment",
    parentSpanId: transaction.spanId
  });
  await processPayment();
  paymentSpan.finish();

  // Span 3: Send confirmation
  const emailSpan = startSpan({
    name: "send-email",
    op: "email",
    parentSpanId: transaction.spanId
  });
  await sendConfirmationEmail();
  emailSpan.finish();

  transaction.setStatus("ok");
} catch (error) {
  transaction.setStatus("error");
  throw error;
} finally {
  transaction.finish();
}

Custom Instrumentation

import { instrument } from "@repo/observability/server/next";

async function fetchUserData(userId: string) {
  // highlight-start
  return await instrument(
    {
      name: "fetch-user-data",
      op: "db.query",
      tags: { userId }
    },
    async (span) => {
      const data = await db.user.findUnique({ where: { id: userId } });
      span.setData("user_found", !!data);
      return data;
    }
  );
  // highlight-end
}

Error Filtering

import { beforeSend } from "@repo/observability/server/next";

// Filter out sensitive data
beforeSend((event) => {
  // highlight-start
  // Remove sensitive headers
  if (event.request?.headers) {
    delete event.request.headers["authorization"];
    delete event.request.headers["cookie"];
  }

  // Filter out specific errors
  if (event.exception?.values?.[0]?.value?.includes("Network error")) {
    return null; // Don't send this event
  }
  // highlight-end

  return event;
});

Testing

Mock Observability

import { createMockObservability } from "@repo/observability/testing";

const mockObservability = createMockObservability();

describe("User Service", () => {
  it("logs user creation", async () => {
    await createUser({ name: "John" });

    // highlight-start
    expect(mockObservability.logInfo).toHaveBeenCalledWith("User created", expect.objectContaining({ name: "John" }));
    // highlight-end
  });
});

Test Utilities

import { clearBreadcrumbs, getLastBreadcrumb } from "@repo/observability/testing";

beforeEach(() => {
  clearBreadcrumbs();
});

it("tracks navigation breadcrumbs", () => {
  navigate("/products");

  const lastBreadcrumb = getLastBreadcrumb();
  expect(lastBreadcrumb?.category).toBe("navigation");
  expect(lastBreadcrumb?.message).toContain("/products");
});

Performance Considerations

Sampling

Production Best Practice

Use sampling to reduce costs and performance impact while maintaining visibility.

// Configure sampling rates
Sentry.init({
  // highlight-start
  tracesSampleRate: 0.1, // 10% of transactions
  profilesSampleRate: 0.1, // 10% of profiling
  replaysSessionSampleRate: 0.1, // 10% of sessions
  replaysOnErrorSampleRate: 1.0 // 100% of error sessions
  // highlight-end
});

Batching

// Batch logs for better performance
const betterStackPlugin = createBetterStackPlugin({
  // highlight-start
  batchSize: 50, // Send logs in batches of 50
  flushInterval: 5000 // Flush every 5 seconds
  // highlight-end
});

Async Logging

import { logInfo } from "@repo/shared";

// Non-blocking logging
logInfo("User action", { userId: "123" });
// Logs directly to console with runtime information

Troubleshooting

Sentry Not Capturing Errors

// ❌ Missing initialization
// Make sure sentry.client.config.ts and sentry.server.config.ts exist

// ✅ Verify DSN is set
console.log(process.env.NEXT_PUBLIC_SENTRY_DSN);

// ✅ Test manually
import * as Sentry from "@sentry/nextjs";
Sentry.captureMessage("Test message");

Edge Runtime Issues

// ❌ Wrong import for edge runtime
import { observability } from "@repo/observability/server/next";

// ✅ Correct import for edge runtime
import { observability } from "@repo/observability/server/edge";

High Volume / Rate Limits

import { logInfo } from "@repo/shared";

// Implement sampling for high-traffic endpoints
if (Math.random() < 0.1) {
  // Only log 10% of requests
  logInfo("High-traffic endpoint hit");
}

External Resources

Contributing

When adding new observability features:

  1. Support all runtimes - client, server, edge
  2. Graceful degradation - continue if providers fail
  3. Add tests - mock providers, validate behavior
  4. Document performance impact - sampling, batching
  5. Follow privacy best practices - filter sensitive data

See packages/observability/README.md for detailed contributor guidelines.

On this page