Pino Logging Setup
Configure structured logging with Pino. Outputs human-readable colorized logs in development and structured JSON in production for log aggregation services.
Step 1: Install Pino
bun add pino pino-prettybun add pino pino-prettypino- Fast JSON logger for Node.jspino-pretty- Pretty-prints logs in development
Step 2: Configure Next.js
Add pino to serverExternalPackages in next.config.ts to prevent Turbopack from bundling pino's dependencies (which include test files that break the build):
const nextConfig: NextConfig = {
// Externalize pino to prevent Turbopack from bundling thread-stream test files
serverExternalPackages: ["pino"],
};const nextConfig: NextConfig = {
// Externalize pino to prevent Turbopack from bundling thread-stream test files
serverExternalPackages: ["pino"],
};Step 3: Add the logger utility
Install via shadcn registry
bunx --bun shadcn@latest add https://fullstackrecipes.com/r/logger.jsonKey features:
- Uses
pino-prettyin development for human-readable colorized output - Outputs JSON in production (for log aggregation services)
- Log level configurable via
PINO_LOG_LEVELenv var (defaults toinfo)
Usage
import { logger } from "@/lib/common/logger";
// Basic logging
logger.info("Server started", { port: 3000 });
logger.warn("Rate limit reached", { endpoint: "/api/chat" });
// Log errors with stack traces
logger.error(err, "Failed to process request");
// Add context
logger.info({ userId: "123", action: "login" }, "User logged in");import { logger } from "@/lib/common/logger";
// Basic logging
logger.info("Server started", { port: 3000 });
logger.warn("Rate limit reached", { endpoint: "/api/chat" });
// Log errors with stack traces
logger.error(err, "Failed to process request");
// Add context
logger.info({ userId: "123", action: "login" }, "User logged in");Log levels (ordered by severity): trace, debug, info, warn, error, fatal
File Structure
src/lib/common/
logger.ts # Pino logger utilitysrc/lib/common/
logger.ts # Pino logger utilityReferences
Sentry Setup
Configure Sentry for error tracking, performance monitoring, and log aggregation. Integrates with Pino to forward logs to Sentry automatically.
Step 1: Run the Sentry Wizard
Create a new Sentry project at sentry.io, then configure your app automatically by running the Sentry wizard in your project root. You can find the personalized command in the Sentry getting-started guide during project creation.
bunx @sentry/wizard@latest -i nextjs --saas --org <org-name> --project <project-name>bunx @sentry/wizard@latest -i nextjs --saas --org <org-name> --project <project-name>Wizard selections:
- Runtime: Bun
- Route requests through your Next.js server: Yes (optional, recommended for privacy)
- Enable Tracing: Yes
- Session Replay: Yes
- Logs: Yes
- Example page: No
- Add Sentry MCP server: Yes
The wizard creates and updates the following TypeScript files:
next.config.ts- Next.js configurationsentry.server.config.ts- Server-side initialization (we'll delete this)sentry.edge.config.ts- Edge runtime initialization (we'll delete this)src/instrumentation-client.ts- Client-side initializationsrc/instrumentation.ts- Instrumentation hooksrc/app/global-error.tsx- Global error component
Step 2: Add environment variables
Add to your .env.development:
NEXT_PUBLIC_ENABLE_SENTRY="true"
NEXT_PUBLIC_SENTRY_DSN="https://your-dsn@sentry.io/your-project-id"
NEXT_PUBLIC_SENTRY_PROJECT="your-project-name"
NEXT_PUBLIC_SENTRY_ORG="your-org-name"
SENTRY_AUTH_TOKEN="your-auth-token"NEXT_PUBLIC_ENABLE_SENTRY="true"
NEXT_PUBLIC_SENTRY_DSN="https://your-dsn@sentry.io/your-project-id"
NEXT_PUBLIC_SENTRY_PROJECT="your-project-name"
NEXT_PUBLIC_SENTRY_ORG="your-org-name"
SENTRY_AUTH_TOKEN="your-auth-token"Then sync to Vercel with bun run env:push.
You can find your Sentry DSN, project name, and org name in your Sentry project settings or within the scaffolded files generated by the Sentry wizard. They're not secrets - they just tell Sentry where to send data. We move these to .env.development to enforce validation and also avoid committing them when working on open source repositories.
SENTRY_AUTH_TOKEN is a secret token added in .env.sentry-build-plugin by the Sentry wizard. You can delete the .env.sentry-build-plugin file after adding the token to .env.development. After that, you can also revert the changes made to .gitignore by removing the .env.sentry-build-plugin line.
Step 3: Create the Sentry config
Create the Sentry config with environment variable validation:
import { loadConfig } from "../common/load-config";
export const sentryConfig = loadConfig({
name: "Sentry",
flag: process.env.NEXT_PUBLIC_ENABLE_SENTRY,
server: {
// SENTRY_AUTH_TOKEN is picked up by the Sentry Build Plugin for source maps upload.
// Accessing this on the client will throw ServerConfigClientAccessError.
token: process.env.SENTRY_AUTH_TOKEN,
},
public: {
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
project: process.env.NEXT_PUBLIC_SENTRY_PROJECT,
org: process.env.NEXT_PUBLIC_SENTRY_ORG,
},
});import { loadConfig } from "../common/load-config";
export const sentryConfig = loadConfig({
name: "Sentry",
flag: process.env.NEXT_PUBLIC_ENABLE_SENTRY,
server: {
// SENTRY_AUTH_TOKEN is picked up by the Sentry Build Plugin for source maps upload.
// Accessing this on the client will throw ServerConfigClientAccessError.
token: process.env.SENTRY_AUTH_TOKEN,
},
public: {
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
project: process.env.NEXT_PUBLIC_SENTRY_PROJECT,
org: process.env.NEXT_PUBLIC_SENTRY_ORG,
},
});We use the loadConfig utility to validate the configuration and throw an error if any of the required environment variables are missing. The config separates server and public sections:
server.*values are only accessible on the server - accessing them on the client throws a helpful errorpublic.*values work everywhere (Next.js inlinesNEXT_PUBLIC_*vars at build time)
Step 4: Create the initialization helpers
Next, we'll refactor the wizard-generated files to use the new Sentry config. We further move the Sentry-related code into the src/lib/sentry directory to keep them organized.
Create the server initialization helper:
import * as Sentry from "@sentry/nextjs";
import { sentryConfig } from "./config";
export function initSentryServer() {
if (!sentryConfig.isEnabled) return;
Sentry.init({
dsn: sentryConfig.public.dsn,
tracesSampleRate: 1,
enableLogs: true,
sendDefaultPii: true,
integrations: [
Sentry.pinoIntegration({ log: { levels: ["info", "warn", "error"] } }),
],
});
}import * as Sentry from "@sentry/nextjs";
import { sentryConfig } from "./config";
export function initSentryServer() {
if (!sentryConfig.isEnabled) return;
Sentry.init({
dsn: sentryConfig.public.dsn,
tracesSampleRate: 1,
enableLogs: true,
sendDefaultPii: true,
integrations: [
Sentry.pinoIntegration({ log: { levels: ["info", "warn", "error"] } }),
],
});
}Create the edge initialization helper:
import * as Sentry from "@sentry/nextjs";
import { sentryConfig } from "./config";
export function initSentryEdge() {
if (!sentryConfig.isEnabled) return;
Sentry.init({
dsn: sentryConfig.public.dsn,
tracesSampleRate: 1,
enableLogs: true,
sendDefaultPii: true,
});
}import * as Sentry from "@sentry/nextjs";
import { sentryConfig } from "./config";
export function initSentryEdge() {
if (!sentryConfig.isEnabled) return;
Sentry.init({
dsn: sentryConfig.public.dsn,
tracesSampleRate: 1,
enableLogs: true,
sendDefaultPii: true,
});
}Note: The pino integration is not included for edge because pino uses Node.js modules (fs, events, worker_threads) that aren't available in the edge runtime.
Create the client initialization helper:
import * as Sentry from "@sentry/nextjs";
import { sentryConfig } from "./config";
export function initSentryClient() {
if (!sentryConfig.isEnabled) return;
Sentry.init({
dsn: sentryConfig.public.dsn,
integrations: [Sentry.replayIntegration()],
tracesSampleRate: 1,
enableLogs: true,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
sendDefaultPii: true,
});
}
export const onRouterTransitionStart = sentryConfig.isEnabled
? Sentry.captureRouterTransitionStart
: () => {};import * as Sentry from "@sentry/nextjs";
import { sentryConfig } from "./config";
export function initSentryClient() {
if (!sentryConfig.isEnabled) return;
Sentry.init({
dsn: sentryConfig.public.dsn,
integrations: [Sentry.replayIntegration()],
tracesSampleRate: 1,
enableLogs: true,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
sendDefaultPii: true,
});
}
export const onRouterTransitionStart = sentryConfig.isEnabled
? Sentry.captureRouterTransitionStart
: () => {};Step 5: Update the wizard-generated files
Delete the wizard-generated sentry.server.config.ts and sentry.edge.config.ts files from the project root. We'll import directly from our src/lib/sentry/ modules instead.
Replace the instrumentation file:
import * as Sentry from "@sentry/nextjs";
import { sentryConfig } from "./lib/sentry/config";
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const { initSentryServer } = await import("./lib/sentry/server");
initSentryServer();
}
if (process.env.NEXT_RUNTIME === "edge") {
const { initSentryEdge } = await import("./lib/sentry/edge");
initSentryEdge();
}
}
export const onRequestError = sentryConfig.isEnabled
? Sentry.captureRequestError
: undefined;import * as Sentry from "@sentry/nextjs";
import { sentryConfig } from "./lib/sentry/config";
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const { initSentryServer } = await import("./lib/sentry/server");
initSentryServer();
}
if (process.env.NEXT_RUNTIME === "edge") {
const { initSentryEdge } = await import("./lib/sentry/edge");
initSentryEdge();
}
}
export const onRequestError = sentryConfig.isEnabled
? Sentry.captureRequestError
: undefined;Replace the client instrumentation file:
// This file configures the initialization of Sentry on the client.
// The added config here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import { initSentryClient, onRouterTransitionStart } from "./lib/sentry/client";
initSentryClient();
export { onRouterTransitionStart };// This file configures the initialization of Sentry on the client.
// The added config here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import { initSentryClient, onRouterTransitionStart } from "./lib/sentry/client";
initSentryClient();
export { onRouterTransitionStart };Step 6: Update next.config.ts
The Sentry wizard updates next.config.ts with hardcoded org and project values. Replace them with environment variables:
// next.config.ts
import type { NextConfig } from "next";
import { withSentryConfig } from "@sentry/nextjs";
const nextConfig: NextConfig = {
/* config options here */
};
export default withSentryConfig(nextConfig, {
// For all available options, see:
// https://www.npmjs.com/package/@sentry/webpack-plugin#options
org: process.env.NEXT_PUBLIC_SENTRY_ORG,
project: process.env.NEXT_PUBLIC_SENTRY_PROJECT,
// Only print logs for uploading source maps in CI
silent: !process.env.CI,
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/monitoring",
webpack: {
// Automatically tree-shake Sentry logger statements to reduce bundle size
treeshake: {
removeDebugLogging: true,
},
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: true,
},
});// next.config.ts
import type { NextConfig } from "next";
import { withSentryConfig } from "@sentry/nextjs";
const nextConfig: NextConfig = {
/* config options here */
};
export default withSentryConfig(nextConfig, {
// For all available options, see:
// https://www.npmjs.com/package/@sentry/webpack-plugin#options
org: process.env.NEXT_PUBLIC_SENTRY_ORG,
project: process.env.NEXT_PUBLIC_SENTRY_PROJECT,
// Only print logs for uploading source maps in CI
silent: !process.env.CI,
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/monitoring",
webpack: {
// Automatically tree-shake Sentry logger statements to reduce bundle size
treeshake: {
removeDebugLogging: true,
},
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: true,
},
});Database query monitoring
When tracing is enabled (tracesSampleRate > 0), Sentry automatically instruments database queries using postgresIntegration. This is included by default - no additional configuration needed.
What you get automatically:
- All
pg(node-postgres) queries captured as spans - Query timing and slow query detection
- Database performance visible in Sentry's Performance tab
- Works with Drizzle ORM since it uses
node-postgresunder the hood
This uses OpenTelemetry instrumentation (@opentelemetry/instrumentation-pg) to hook into the pg library. Supports pg versions 8.x.
To disable or customize (if needed):
Sentry.init({
dsn: "...",
tracesSampleRate: 1,
integrations: (defaults) => {
// Remove postgres integration
return defaults.filter((i) => i.name !== "Postgres");
},
});Sentry.init({
dsn: "...",
tracesSampleRate: 1,
integrations: (defaults) => {
// Remove postgres integration
return defaults.filter((i) => i.name !== "Postgres");
},
});Step 7: Create Cursor rules for Sentry
Create .cursor/rules/sentry.mdc to help AI coding agents use Sentry APIs correctly:
---
description: Sentry error monitoring and tracking
alwaysApply: false
---
These examples should be used as guidance when configuring Sentry functionality within a project.
# Exception Catching
Use `Sentry.captureException(error)` to capture an exception and log the error in Sentry.
Use this in try catch blocks or areas where exceptions are expected
# Tracing Examples
Spans should be created for meaningful actions within an applications like button clicks, API calls, and function calls
Use the `Sentry.startSpan` function to create a span
Child spans can exist within a parent span
## Custom Span instrumentation in component actions
The `name` and `op` properties should be meaninful for the activities in the call.
Attach attributes based on relevant information and metrics from the request
```tsx
function TestComponent() {
const handleTestButtonClick = () => {
// Create a transaction/span to measure performance
Sentry.startSpan(
{
op: "ui.click",
name: "Test Button Click",
},
(span) => {
const value = "some config";
const metric = "some metric";
// Metrics can be added to the span
span.setAttribute("config", value);
span.setAttribute("metric", metric);
doSomething();
},
);
};
return (
<button type="button" onClick={handleTestButtonClick}>
Test Sentry
</button>
);
}
```
## Custom span instrumentation in API calls
The `name` and `op` properties should be meaninful for the activities in the call.
Attach attributes based on relevant information and metrics from the request
```typescript
async function fetchUserData(userId) {
return Sentry.startSpan(
{
op: "http.client",
name: `GET /api/users/${userId}`,
},
async () => {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return data;
},
);
}
```
# Logs
Where logs are used, ensure Sentry is imported using `import * as Sentry from "@sentry/nextjs"`
Enable logging in Sentry using `Sentry.init({ enableLogs: true })`
Reference the logger using `const { logger } = Sentry`
Sentry offers a consoleLoggingIntegration that can be used to log specific console error types automatically without instrumenting the individual logger calls
## Configuration
In NextJS the client side Sentry initialization is in `instrumentation-client.(js|ts)`, the server initialization is in `sentry.server.config.ts` and the edge initialization is in `sentry.edge.config.ts`
Initialization does not need to be repeated in other files, it only needs to happen the files mentioned above. You should use `import * as Sentry from "@sentry/nextjs"` to reference Sentry functionality
### Baseline
```typescript
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "https://your-dsn@sentry.io/your-project-id",
enableLogs: true,
});
```
### Logger Integration
```typescript
Sentry.init({
dsn: "https://your-dsn@sentry.io/your-project-id",
integrations: [
// send console.log, console.warn, and console.error calls as logs to Sentry
Sentry.consoleLoggingIntegration({ levels: ["log", "warn", "error"] }),
],
});
```
## Logger Examples
`logger.fmt` is a template literal function that should be used to bring variables into the structured logs.
```typescript
logger.trace("Starting database connection", { database: "users" });
logger.debug(logger.fmt`Cache miss for user: ${userId}`);
logger.info("Updated profile", { profileId: 345 });
logger.warn("Rate limit reached for endpoint", {
endpoint: "/api/results/",
isEnterprise: false,
});
logger.error("Failed to process payment", {
orderId: "order_123",
amount: 99.99,
});
logger.fatal("Database connection pool exhausted", {
database: "users",
activeConnections: 100,
});
```---
description: Sentry error monitoring and tracking
alwaysApply: false
---
These examples should be used as guidance when configuring Sentry functionality within a project.
# Exception Catching
Use `Sentry.captureException(error)` to capture an exception and log the error in Sentry.
Use this in try catch blocks or areas where exceptions are expected
# Tracing Examples
Spans should be created for meaningful actions within an applications like button clicks, API calls, and function calls
Use the `Sentry.startSpan` function to create a span
Child spans can exist within a parent span
## Custom Span instrumentation in component actions
The `name` and `op` properties should be meaninful for the activities in the call.
Attach attributes based on relevant information and metrics from the request
```tsx
function TestComponent() {
const handleTestButtonClick = () => {
// Create a transaction/span to measure performance
Sentry.startSpan(
{
op: "ui.click",
name: "Test Button Click",
},
(span) => {
const value = "some config";
const metric = "some metric";
// Metrics can be added to the span
span.setAttribute("config", value);
span.setAttribute("metric", metric);
doSomething();
},
);
};
return (
<button type="button" onClick={handleTestButtonClick}>
Test Sentry
</button>
);
}
```
## Custom span instrumentation in API calls
The `name` and `op` properties should be meaninful for the activities in the call.
Attach attributes based on relevant information and metrics from the request
```typescript
async function fetchUserData(userId) {
return Sentry.startSpan(
{
op: "http.client",
name: `GET /api/users/${userId}`,
},
async () => {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return data;
},
);
}
```
# Logs
Where logs are used, ensure Sentry is imported using `import * as Sentry from "@sentry/nextjs"`
Enable logging in Sentry using `Sentry.init({ enableLogs: true })`
Reference the logger using `const { logger } = Sentry`
Sentry offers a consoleLoggingIntegration that can be used to log specific console error types automatically without instrumenting the individual logger calls
## Configuration
In NextJS the client side Sentry initialization is in `instrumentation-client.(js|ts)`, the server initialization is in `sentry.server.config.ts` and the edge initialization is in `sentry.edge.config.ts`
Initialization does not need to be repeated in other files, it only needs to happen the files mentioned above. You should use `import * as Sentry from "@sentry/nextjs"` to reference Sentry functionality
### Baseline
```typescript
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "https://your-dsn@sentry.io/your-project-id",
enableLogs: true,
});
```
### Logger Integration
```typescript
Sentry.init({
dsn: "https://your-dsn@sentry.io/your-project-id",
integrations: [
// send console.log, console.warn, and console.error calls as logs to Sentry
Sentry.consoleLoggingIntegration({ levels: ["log", "warn", "error"] }),
],
});
```
## Logger Examples
`logger.fmt` is a template literal function that should be used to bring variables into the structured logs.
```typescript
logger.trace("Starting database connection", { database: "users" });
logger.debug(logger.fmt`Cache miss for user: ${userId}`);
logger.info("Updated profile", { profileId: 345 });
logger.warn("Rate limit reached for endpoint", {
endpoint: "/api/results/",
isEnterprise: false,
});
logger.error("Failed to process payment", {
orderId: "order_123",
amount: 99.99,
});
logger.fatal("Database connection pool exhausted", {
database: "users",
activeConnections: 100,
});
```File Structure
After setup, you'll have these Sentry-related files:
src/
instrumentation.ts # Next.js instrumentation hook - calls initSentryServer() or initSentryEdge()
instrumentation-client.ts # Client instrumentation - calls initSentryClient()
lib/sentry/
config.ts # Validates NEXT_PUBLIC_SENTRY_DSN, NEXT_PUBLIC_SENTRY_PROJECT, NEXT_PUBLIC_SENTRY_ORG
server.ts # initSentryServer() with pino integration
edge.ts # initSentryEdge() without pino (edge doesn't support Node.js modules)
client.ts # initSentryClient() with replay integration
.cursor/rules/
sentry.mdc # AI agent guidelines for Sentrysrc/
instrumentation.ts # Next.js instrumentation hook - calls initSentryServer() or initSentryEdge()
instrumentation-client.ts # Client instrumentation - calls initSentryClient()
lib/sentry/
config.ts # Validates NEXT_PUBLIC_SENTRY_DSN, NEXT_PUBLIC_SENTRY_PROJECT, NEXT_PUBLIC_SENTRY_ORG
server.ts # initSentryServer() with pino integration
edge.ts # initSentryEdge() without pino (edge doesn't support Node.js modules)
client.ts # initSentryClient() with replay integration
.cursor/rules/
sentry.mdc # AI agent guidelines for SentryReferences
Vercel Web Analytics
Add privacy-focused web analytics with Vercel Web Analytics. Track page views, visitors, and custom events with zero configuration.
Step 1: Install the package
bun add @vercel/analyticsbun add @vercel/analyticsStep 2: Add the Analytics component
Add the Analytics component to your root layout:
import { Analytics } from "@vercel/analytics/next";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<Analytics />
</body>
</html>
);
}import { Analytics } from "@vercel/analytics/next";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<Analytics />
</body>
</html>
);
}That's it! Page views are now tracked automatically.
Custom Events
Track custom events to measure user actions:
import { track } from "@vercel/analytics";
// Track a button click
function SignupButton() {
return (
<button onClick={() => track("signup_clicked")}>
Sign Up
</button>
);
}
// Track with properties
track("purchase_completed", {
plan: "pro",
price: 29,
currency: "USD",
});
// Track form submissions
function ContactForm() {
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
track("contact_form_submitted", { source: "footer" });
// ... submit form
};
return <form onSubmit={handleSubmit}>...</form>;
}import { track } from "@vercel/analytics";
// Track a button click
function SignupButton() {
return (
<button onClick={() => track("signup_clicked")}>
Sign Up
</button>
);
}
// Track with properties
track("purchase_completed", {
plan: "pro",
price: 29,
currency: "USD",
});
// Track form submissions
function ContactForm() {
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
track("contact_form_submitted", { source: "footer" });
// ... submit form
};
return <form onSubmit={handleSubmit}>...</form>;
}Development
Analytics are only sent in production by default. To test in development, set the mode prop:
<Analytics mode="development" /><Analytics mode="development" />Or use the debug prop to log events to the console:
<Analytics debug /><Analytics debug />References
Working with Logging
Use structured logging with Pino throughout your application. Covers log levels, context, and workflow-safe logging patterns.
Implement Working with Logging
Use structured logging with Pino throughout your application. Covers log levels, context, and workflow-safe logging patterns.
See:
- Resource:
using-loggingin Fullstack Recipes - URL: https://fullstackrecipes.com/recipes/using-logging
Basic Logging
Import the logger and use it throughout your application:
import { logger } from "@/lib/common/logger";
// Info level for normal operations
logger.info("Server started", { port: 3000 });
// Warn level for recoverable issues
logger.warn("Rate limit reached", { endpoint: "/api/chat" });
// Error level with Error objects
logger.error(err, "Failed to process request");
// Debug level for development troubleshooting
logger.debug("Cache miss", { key: "user:123" });import { logger } from "@/lib/common/logger";
// Info level for normal operations
logger.info("Server started", { port: 3000 });
// Warn level for recoverable issues
logger.warn("Rate limit reached", { endpoint: "/api/chat" });
// Error level with Error objects
logger.error(err, "Failed to process request");
// Debug level for development troubleshooting
logger.debug("Cache miss", { key: "user:123" });Structured Logging
Always include context as the first argument for structured logs:
// Context object first, message second
logger.info({ userId: "123", action: "login" }, "User logged in");
// For errors, pass the error first
logger.error({ err, userId: "123", endpoint: "/api/chat" }, "Request failed");// Context object first, message second
logger.info({ userId: "123", action: "login" }, "User logged in");
// For errors, pass the error first
logger.error({ err, userId: "123", endpoint: "/api/chat" }, "Request failed");Log Levels
Use appropriate levels for different scenarios:
| Level | When to Use |
|---|---|
trace | Detailed debugging (rarely used) |
debug | Development troubleshooting |
info | Normal operations, business events |
warn | Recoverable issues, deprecation warnings |
error | Failures that need attention |
fatal | Critical failures, app cannot continue |
Configuring Log Level
Set the PINO_LOG_LEVEL environment variable:
# Show all logs including debug
PINO_LOG_LEVEL="debug"
# Production: only warnings and errors
PINO_LOG_LEVEL="warn"# Show all logs including debug
PINO_LOG_LEVEL="debug"
# Production: only warnings and errors
PINO_LOG_LEVEL="warn"Default is info if not set.
Logging in API Routes
import { logger } from "@/lib/common/logger";
export async function POST(request: Request) {
const start = Date.now();
try {
const result = await processRequest(request);
logger.info(
{ duration: Date.now() - start, status: 200 },
"Request completed",
);
return Response.json(result);
} catch (err) {
logger.error({ err, duration: Date.now() - start }, "Request failed");
return Response.json({ error: "Internal error" }, { status: 500 });
}
}import { logger } from "@/lib/common/logger";
export async function POST(request: Request) {
const start = Date.now();
try {
const result = await processRequest(request);
logger.info(
{ duration: Date.now() - start, status: 200 },
"Request completed",
);
return Response.json(result);
} catch (err) {
logger.error({ err, duration: Date.now() - start }, "Request failed");
return Response.json({ error: "Internal error" }, { status: 500 });
}
}Logging in Workflows
Workflow functions run in a restricted environment. Use the logger step wrapper:
import { logger } from "@/lib/common/logger";
export async function log(
level: "info" | "warn" | "error" | "debug",
message: string,
data?: Record<string, unknown>,
): Promise<void> {
"use step";
if (data) {
logger[level](data, message);
} else {
logger[level](message);
}
}import { logger } from "@/lib/common/logger";
export async function log(
level: "info" | "warn" | "error" | "debug",
message: string,
data?: Record<string, unknown>,
): Promise<void> {
"use step";
if (data) {
logger[level](data, message);
} else {
logger[level](message);
}
}Then use it in workflows:
import { log } from "./steps/logger";
export async function chatWorkflow({ chatId }) {
"use workflow";
await log("info", "Workflow started", { chatId });
}import { log } from "./steps/logger";
export async function chatWorkflow({ chatId }) {
"use workflow";
await log("info", "Workflow started", { chatId });
}References
Working with Sentry
Capture exceptions, add context, create performance spans, and use structured logging with Sentry.
Implement Working with Sentry
Capture exceptions, add context, create performance spans, and use structured logging with Sentry.
See:
- Resource:
using-sentryin Fullstack Recipes - URL: https://fullstackrecipes.com/recipes/using-sentry
Capturing Exceptions
Manually capture errors that are handled but should be tracked:
import * as Sentry from "@sentry/nextjs";
try {
await riskyOperation();
} catch (err) {
Sentry.captureException(err);
// Handle the error gracefully...
}import * as Sentry from "@sentry/nextjs";
try {
await riskyOperation();
} catch (err) {
Sentry.captureException(err);
// Handle the error gracefully...
}Adding Context
Attach user and custom context to errors:
import * as Sentry from "@sentry/nextjs";
// Set user context (persists for session)
Sentry.setUser({
id: session.user.id,
email: session.user.email,
});
// Add custom context to exceptions
Sentry.captureException(err, {
tags: {
feature: "checkout",
plan: "pro",
},
extra: {
orderId: "order_123",
items: cart.items,
},
});import * as Sentry from "@sentry/nextjs";
// Set user context (persists for session)
Sentry.setUser({
id: session.user.id,
email: session.user.email,
});
// Add custom context to exceptions
Sentry.captureException(err, {
tags: {
feature: "checkout",
plan: "pro",
},
extra: {
orderId: "order_123",
items: cart.items,
},
});Performance Tracing
Create spans for meaningful operations:
import * as Sentry from "@sentry/nextjs";
// Wrap async operations
const result = await Sentry.startSpan(
{
op: "http.client",
name: "GET /api/users",
},
async () => {
const response = await fetch("/api/users");
return response.json();
},
);
// Wrap sync operations
Sentry.startSpan(
{
op: "ui.click",
name: "Submit Button Click",
},
(span) => {
span.setAttribute("form", "checkout");
processSubmit();
},
);import * as Sentry from "@sentry/nextjs";
// Wrap async operations
const result = await Sentry.startSpan(
{
op: "http.client",
name: "GET /api/users",
},
async () => {
const response = await fetch("/api/users");
return response.json();
},
);
// Wrap sync operations
Sentry.startSpan(
{
op: "ui.click",
name: "Submit Button Click",
},
(span) => {
span.setAttribute("form", "checkout");
processSubmit();
},
);Using the Sentry Logger
Sentry provides structured logging that appears in the Logs tab:
import * as Sentry from "@sentry/nextjs";
const { logger } = Sentry;
logger.info("Payment processed", { orderId: "123", amount: 99.99 });
logger.warn("Rate limit approaching", { current: 90, max: 100 });
logger.error("Payment failed", { orderId: "123", reason: "declined" });import * as Sentry from "@sentry/nextjs";
const { logger } = Sentry;
logger.info("Payment processed", { orderId: "123", amount: 99.99 });
logger.warn("Rate limit approaching", { current: 90, max: 100 });
logger.error("Payment failed", { orderId: "123", reason: "declined" });Breadcrumbs
Add breadcrumbs to provide context for errors:
import * as Sentry from "@sentry/nextjs";
// Automatically captured: console logs, fetch requests, UI clicks
// Manual breadcrumbs for custom events:
Sentry.addBreadcrumb({
category: "auth",
message: "User signed in",
level: "info",
});import * as Sentry from "@sentry/nextjs";
// Automatically captured: console logs, fetch requests, UI clicks
// Manual breadcrumbs for custom events:
Sentry.addBreadcrumb({
category: "auth",
message: "User signed in",
level: "info",
});Clearing User Context
Clear user data on sign out:
import * as Sentry from "@sentry/nextjs";
async function signOut() {
Sentry.setUser(null);
await authClient.signOut();
}import * as Sentry from "@sentry/nextjs";
async function signOut() {
Sentry.setUser(null);
await authClient.signOut();
}References
Working with Analytics
Track custom events and conversions with Vercel Web Analytics. Covers common events, form tracking, and development testing.
Implement Working with Analytics
Track custom events and conversions with Vercel Web Analytics. Covers common events, form tracking, and development testing.
See:
- Resource:
using-analyticsin Fullstack Recipes - URL: https://fullstackrecipes.com/recipes/using-analytics
Tracking Custom Events
Track user actions and conversions:
import { track } from "@vercel/analytics";
// Basic event
track("signup_clicked");
// Event with properties
track("purchase_completed", {
plan: "pro",
price: 29,
currency: "USD",
});import { track } from "@vercel/analytics";
// Basic event
track("signup_clicked");
// Event with properties
track("purchase_completed", {
plan: "pro",
price: 29,
currency: "USD",
});Common Events to Track
Track meaningful user actions:
// Authentication
track("signup_completed", { method: "email" });
track("signin_completed", { method: "google" });
// Feature usage
track("chat_started");
track("chat_completed", { messageCount: 5 });
track("file_uploaded", { type: "pdf", size: 1024 });
// Conversions
track("trial_started");
track("subscription_created", { plan: "pro" });
track("upgrade_completed", { from: "free", to: "pro" });// Authentication
track("signup_completed", { method: "email" });
track("signin_completed", { method: "google" });
// Feature usage
track("chat_started");
track("chat_completed", { messageCount: 5 });
track("file_uploaded", { type: "pdf", size: 1024 });
// Conversions
track("trial_started");
track("subscription_created", { plan: "pro" });
track("upgrade_completed", { from: "free", to: "pro" });Tracking in Components
import { track } from "@vercel/analytics";
function UpgradeButton() {
const handleClick = () => {
track("upgrade_button_clicked", { location: "header" });
// Navigate to upgrade page...
};
return <button onClick={handleClick}>Upgrade</button>;
}import { track } from "@vercel/analytics";
function UpgradeButton() {
const handleClick = () => {
track("upgrade_button_clicked", { location: "header" });
// Navigate to upgrade page...
};
return <button onClick={handleClick}>Upgrade</button>;
}Tracking Form Submissions
import { track } from "@vercel/analytics";
function ContactForm() {
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
track("contact_form_submitted", { source: "footer" });
// Submit form...
};
return <form onSubmit={handleSubmit}>...</form>;
}import { track } from "@vercel/analytics";
function ContactForm() {
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
track("contact_form_submitted", { source: "footer" });
// Submit form...
};
return <form onSubmit={handleSubmit}>...</form>;
}Testing in Development
Analytics only send in production by default. For development testing:
// In layout.tsx
<Analytics mode="development" />
// Or just log to console
<Analytics debug />// In layout.tsx
<Analytics mode="development" />
// Or just log to console
<Analytics debug />Viewing Analytics
View analytics in the Vercel dashboard:
- Go to your project in Vercel Dashboard
- Click "Analytics" in the sidebar
- View page views, visitors, and custom events