@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/observabilityLog 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/observability2. Configure Sentry (optional)
NEXT_PUBLIC_SENTRY_DSN=https://xxx@sentry.io/xxx
SENTRY_ORG=your-org
SENTRY_PROJECT=your-project3. Add logging to a client component
"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
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:
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
| Property | Value |
|---|---|
| Location | packages/observability |
| Version | 2025.11.1201 |
| Dependencies | @sentry/nextjs, @logtape/logtape, PostHog |
| Integrations | Sentry, BetterStack, Console, LogTape |
| Build | TypeScript library (no build step required) |
Export Paths
| Path | Description |
|---|---|
@repo/observability | Shared utilities and types |
@repo/observability/client | Browser (non-Next.js) |
@repo/observability/server | Node.js server |
@repo/observability/client/next | Next.js client components |
@repo/observability/server/next | Next.js server components |
@repo/observability/server/edge | Edge runtime (middleware) |
@repo/observability/env | Environment configuration |
@repo/observability/plugins/* | Provider-specific plugins |
Plugin Exports
| Path | Description |
|---|---|
@repo/observability/plugins/console | Console logging plugin |
@repo/observability/plugins/sentry | Sentry error tracking |
@repo/observability/plugins/betterstack | BetterStack logging |
@repo/observability/plugins/logtape | LogTape integration |
@repo/observability/plugins/sentry-microfrontend | Micro-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-endError 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
| Metric | Description | Good | Needs Improvement | Poor |
|---|---|---|---|---|
| LCP | Largest Contentful Paint | ≤2.5s | 2.5s-4s | >4s |
| FID | First Input Delay | ≤100ms | 100ms-300ms | >300ms |
| CLS | Cumulative Layout Shift | ≤0.1 | 0.1-0.25 | >0.25 |
| FCP | First Contentful Paint | ≤1.8s | 1.8s-3s | >3s |
| TTFB | Time to First Byte | ≤800ms | 800ms-1800ms | >1800ms |
| INP | Interaction to Next Paint (NEW 2024) | ≤200ms | 200ms-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
});Breadcrumbs
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-endEnvironment 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.0Runtime 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 componentsAdvanced 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 informationTroubleshooting
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");
}Related Packages
- @repo/ai - AI operation tracing
- @repo/ai-oneapp - Enterprise AI with observability
- @repo/analytics - Usage analytics and tracking
- @repo/security - Security event logging
External Resources
Contributing
When adding new observability features:
- Support all runtimes - client, server, edge
- Graceful degradation - continue if providers fail
- Add tests - mock providers, validate behavior
- Document performance impact - sampling, batching
- Follow privacy best practices - filter sensitive data
See packages/observability/README.md for detailed contributor guidelines.