Observability is a critical aspect of modern web development, enabling you to monitor, trace, and debug your application's performance and errors. TanStack Start provides built-in patterns for observability and integrates seamlessly with external tools to give you comprehensive insights into your application.
For comprehensive observability, we recommend Sentry - our trusted partner for error tracking and performance monitoring. Sentry provides:
Quick Setup:
// Client-side (app.tsx)
import * as Sentry from '@sentry/react'
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.NODE_ENV,
})
// Server functions
import * as Sentry from '@sentry/node'
const serverFn = createServerFn().handler(async () => {
try {
return await riskyOperation()
} catch (error) {
Sentry.captureException(error)
throw error
}
})
// Client-side (app.tsx)
import * as Sentry from '@sentry/react'
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.NODE_ENV,
})
// Server functions
import * as Sentry from '@sentry/node'
const serverFn = createServerFn().handler(async () => {
try {
return await riskyOperation()
} catch (error) {
Sentry.captureException(error)
throw error
}
})
Get started with Sentry → | View integration example →
TanStack Start's architecture provides several opportunities for built-in observability without external dependencies:
Add logging to your server functions to track execution, performance, and errors:
import { createServerFn } from '@tanstack/react-start'
const getUser = createServerFn({ method: 'GET' })
.inputValidator((id: string) => id)
.handler(async ({ data: id }) => {
const startTime = Date.now()
try {
console.log(`[SERVER] Fetching user ${id}`)
const user = await db.users.findUnique({ where: { id } })
if (!user) {
console.log(`[SERVER] User ${id} not found`)
throw new Error('User not found')
}
const duration = Date.now() - startTime
console.log(`[SERVER] User ${id} fetched in ${duration}ms`)
return user
} catch (error) {
const duration = Date.now() - startTime
console.error(
`[SERVER] Error fetching user ${id} after ${duration}ms:`,
error,
)
throw error
}
})
import { createServerFn } from '@tanstack/react-start'
const getUser = createServerFn({ method: 'GET' })
.inputValidator((id: string) => id)
.handler(async ({ data: id }) => {
const startTime = Date.now()
try {
console.log(`[SERVER] Fetching user ${id}`)
const user = await db.users.findUnique({ where: { id } })
if (!user) {
console.log(`[SERVER] User ${id} not found`)
throw new Error('User not found')
}
const duration = Date.now() - startTime
console.log(`[SERVER] User ${id} fetched in ${duration}ms`)
return user
} catch (error) {
const duration = Date.now() - startTime
console.error(
`[SERVER] Error fetching user ${id} after ${duration}ms:`,
error,
)
throw error
}
})
Create middleware to log all requests and responses:
import { createMiddleware } from '@tanstack/react-start'
const requestLogger = createMiddleware().handler(async ({ next }) => {
const startTime = Date.now()
const timestamp = new Date().toISOString()
console.log(`[${timestamp}] ${request.method} ${request.url} - Starting`)
try {
const response = await next()
const duration = Date.now() - startTime
console.log(
`[${timestamp}] ${request.method} ${request.url} - ${response.status} (${duration}ms)`,
)
return response
} catch (error) {
const duration = Date.now() - startTime
console.error(
`[${timestamp}] ${request.method} ${request.url} - Error (${duration}ms):`,
error,
)
throw error
}
})
// Apply to all server routes
export const Route = createFileRoute('/api/users')({
server: {
middleware: [requestLogger],
handlers: {
GET: async () => {
return json({ users: await getUsers() })
},
},
},
})
import { createMiddleware } from '@tanstack/react-start'
const requestLogger = createMiddleware().handler(async ({ next }) => {
const startTime = Date.now()
const timestamp = new Date().toISOString()
console.log(`[${timestamp}] ${request.method} ${request.url} - Starting`)
try {
const response = await next()
const duration = Date.now() - startTime
console.log(
`[${timestamp}] ${request.method} ${request.url} - ${response.status} (${duration}ms)`,
)
return response
} catch (error) {
const duration = Date.now() - startTime
console.error(
`[${timestamp}] ${request.method} ${request.url} - Error (${duration}ms):`,
error,
)
throw error
}
})
// Apply to all server routes
export const Route = createFileRoute('/api/users')({
server: {
middleware: [requestLogger],
handlers: {
GET: async () => {
return json({ users: await getUsers() })
},
},
},
})
Track route loading performance on both client and server:
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
loader: async ({ context }) => {
const startTime = Date.now()
try {
const data = await loadDashboardData()
const duration = Date.now() - startTime
// Log server-side performance
if (typeof window === 'undefined') {
console.log(`[SSR] Dashboard loaded in ${duration}ms`)
}
return data
} catch (error) {
const duration = Date.now() - startTime
console.error(`[LOADER] Dashboard error after ${duration}ms:`, error)
throw error
}
},
component: Dashboard,
})
function Dashboard() {
const data = Route.useLoaderData()
// Track client-side render time
React.useEffect(() => {
const renderTime = performance.now()
console.log(`[CLIENT] Dashboard rendered in ${renderTime}ms`)
}, [])
return <div>Dashboard content</div>
}
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
loader: async ({ context }) => {
const startTime = Date.now()
try {
const data = await loadDashboardData()
const duration = Date.now() - startTime
// Log server-side performance
if (typeof window === 'undefined') {
console.log(`[SSR] Dashboard loaded in ${duration}ms`)
}
return data
} catch (error) {
const duration = Date.now() - startTime
console.error(`[LOADER] Dashboard error after ${duration}ms:`, error)
throw error
}
},
component: Dashboard,
})
function Dashboard() {
const data = Route.useLoaderData()
// Track client-side render time
React.useEffect(() => {
const renderTime = performance.now()
console.log(`[CLIENT] Dashboard rendered in ${renderTime}ms`)
}, [])
return <div>Dashboard content</div>
}
Create server routes for health monitoring:
// routes/health.ts
import { createFileRoute } from '@tanstack/react-router'
import { json } from '@tanstack/react-start'
export const Route = createFileRoute('/health')({
server: {
handlers: {
GET: async () => {
const checks = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
database: await checkDatabase(),
version: process.env.npm_package_version,
}
return json(checks)
},
},
},
})
async function checkDatabase() {
try {
await db.raw('SELECT 1')
return { status: 'connected', latency: 0 }
} catch (error) {
return { status: 'error', error: error.message }
}
}
// routes/health.ts
import { createFileRoute } from '@tanstack/react-router'
import { json } from '@tanstack/react-start'
export const Route = createFileRoute('/health')({
server: {
handlers: {
GET: async () => {
const checks = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
database: await checkDatabase(),
version: process.env.npm_package_version,
}
return json(checks)
},
},
},
})
async function checkDatabase() {
try {
await db.raw('SELECT 1')
return { status: 'connected', latency: 0 }
} catch (error) {
return { status: 'error', error: error.message }
}
}
Implement comprehensive error handling:
// Client-side error boundary
import { ErrorBoundary } from 'react-error-boundary'
function ErrorFallback({ error, resetErrorBoundary }: any) {
// Log client errors
console.error('[CLIENT ERROR]:', error)
// Could also send to external service
// sendErrorToService(error)
return (
<div role="alert">
<h2>Something went wrong</h2>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)
}
export function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Router />
</ErrorBoundary>
)
}
// Server function error handling
const riskyOperation = createServerFn().handler(async () => {
try {
return await performOperation()
} catch (error) {
// Log server errors with context
console.error('[SERVER ERROR]:', {
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
// Add request context if available
})
// Return user-friendly error
throw new Error('Operation failed. Please try again.')
}
})
// Client-side error boundary
import { ErrorBoundary } from 'react-error-boundary'
function ErrorFallback({ error, resetErrorBoundary }: any) {
// Log client errors
console.error('[CLIENT ERROR]:', error)
// Could also send to external service
// sendErrorToService(error)
return (
<div role="alert">
<h2>Something went wrong</h2>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)
}
export function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Router />
</ErrorBoundary>
)
}
// Server function error handling
const riskyOperation = createServerFn().handler(async () => {
try {
return await performOperation()
} catch (error) {
// Log server errors with context
console.error('[SERVER ERROR]:', {
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
// Add request context if available
})
// Return user-friendly error
throw new Error('Operation failed. Please try again.')
}
})
Collect and expose basic performance metrics:
// utils/metrics.ts
class MetricsCollector {
private metrics = new Map<string, number[]>()
recordTiming(name: string, duration: number) {
if (!this.metrics.has(name)) {
this.metrics.set(name, [])
}
this.metrics.get(name)!.push(duration)
}
getStats(name: string) {
const timings = this.metrics.get(name) || []
if (timings.length === 0) return null
const sorted = timings.sort((a, b) => a - b)
return {
count: timings.length,
avg: timings.reduce((a, b) => a + b, 0) / timings.length,
p50: sorted[Math.floor(sorted.length * 0.5)],
p95: sorted[Math.floor(sorted.length * 0.95)],
min: sorted[0],
max: sorted[sorted.length - 1],
}
}
getAllStats() {
const stats: Record<string, any> = {}
for (const [name] of this.metrics) {
stats[name] = this.getStats(name)
}
return stats
}
}
export const metrics = new MetricsCollector()
// Metrics endpoint
// routes/metrics.ts
export const Route = createFileRoute('/metrics')({
server: {
handlers: {
GET: async () => {
return json({
system: {
uptime: process.uptime(),
memory: process.memoryUsage(),
timestamp: new Date().toISOString(),
},
application: metrics.getAllStats(),
})
},
},
},
})
// utils/metrics.ts
class MetricsCollector {
private metrics = new Map<string, number[]>()
recordTiming(name: string, duration: number) {
if (!this.metrics.has(name)) {
this.metrics.set(name, [])
}
this.metrics.get(name)!.push(duration)
}
getStats(name: string) {
const timings = this.metrics.get(name) || []
if (timings.length === 0) return null
const sorted = timings.sort((a, b) => a - b)
return {
count: timings.length,
avg: timings.reduce((a, b) => a + b, 0) / timings.length,
p50: sorted[Math.floor(sorted.length * 0.5)],
p95: sorted[Math.floor(sorted.length * 0.95)],
min: sorted[0],
max: sorted[sorted.length - 1],
}
}
getAllStats() {
const stats: Record<string, any> = {}
for (const [name] of this.metrics) {
stats[name] = this.getStats(name)
}
return stats
}
}
export const metrics = new MetricsCollector()
// Metrics endpoint
// routes/metrics.ts
export const Route = createFileRoute('/metrics')({
server: {
handlers: {
GET: async () => {
return json({
system: {
uptime: process.uptime(),
memory: process.memoryUsage(),
timestamp: new Date().toISOString(),
},
application: metrics.getAllStats(),
})
},
},
},
})
Add helpful debug information to responses:
import { createMiddleware } from '@tanstack/react-start'
const debugMiddleware = createMiddleware().handler(async ({ next }) => {
const response = await next()
if (process.env.NODE_ENV === 'development') {
response.headers.set('X-Debug-Timestamp', new Date().toISOString())
response.headers.set('X-Debug-Node-Version', process.version)
response.headers.set('X-Debug-Uptime', process.uptime().toString())
}
return response
})
import { createMiddleware } from '@tanstack/react-start'
const debugMiddleware = createMiddleware().handler(async ({ next }) => {
const response = await next()
if (process.env.NODE_ENV === 'development') {
response.headers.set('X-Debug-Timestamp', new Date().toISOString())
response.headers.set('X-Debug-Node-Version', process.version)
response.headers.set('X-Debug-Uptime', process.uptime().toString())
}
return response
})
Configure different logging strategies for development vs production:
// utils/logger.ts
import { createIsomorphicFn } from '@tanstack/react-start'
type LogLevel = 'debug' | 'info' | 'warn' | 'error'
const logger = createIsomorphicFn()
.server((level: LogLevel, message: string, data?: any) => {
const timestamp = new Date().toISOString()
if (process.env.NODE_ENV === 'development') {
// Development: Detailed console logging
console[level](`[${timestamp}] [${level.toUpperCase()}]`, message, data)
} else {
// Production: Structured JSON logging
console.log(
JSON.stringify({
timestamp,
level,
message,
data,
service: 'tanstack-start',
environment: process.env.NODE_ENV,
}),
)
}
})
.client((level: LogLevel, message: string, data?: any) => {
if (process.env.NODE_ENV === 'development') {
console[level](`[CLIENT] [${level.toUpperCase()}]`, message, data)
} else {
// Production: Send to analytics service
// analytics.track('client_log', { level, message, data })
}
})
// Usage anywhere in your app
export { logger }
// Example usage
const fetchUserData = createServerFn().handler(async ({ data: userId }) => {
logger('info', 'Fetching user data', { userId })
try {
const user = await db.users.findUnique({ where: { id: userId } })
logger('info', 'User data fetched successfully', { userId })
return user
} catch (error) {
logger('error', 'Failed to fetch user data', {
userId,
error: error.message,
})
throw error
}
})
// utils/logger.ts
import { createIsomorphicFn } from '@tanstack/react-start'
type LogLevel = 'debug' | 'info' | 'warn' | 'error'
const logger = createIsomorphicFn()
.server((level: LogLevel, message: string, data?: any) => {
const timestamp = new Date().toISOString()
if (process.env.NODE_ENV === 'development') {
// Development: Detailed console logging
console[level](`[${timestamp}] [${level.toUpperCase()}]`, message, data)
} else {
// Production: Structured JSON logging
console.log(
JSON.stringify({
timestamp,
level,
message,
data,
service: 'tanstack-start',
environment: process.env.NODE_ENV,
}),
)
}
})
.client((level: LogLevel, message: string, data?: any) => {
if (process.env.NODE_ENV === 'development') {
console[level](`[CLIENT] [${level.toUpperCase()}]`, message, data)
} else {
// Production: Send to analytics service
// analytics.track('client_log', { level, message, data })
}
})
// Usage anywhere in your app
export { logger }
// Example usage
const fetchUserData = createServerFn().handler(async ({ data: userId }) => {
logger('info', 'Fetching user data', { userId })
try {
const user = await db.users.findUnique({ where: { id: userId } })
logger('info', 'User data fetched successfully', { userId })
return user
} catch (error) {
logger('error', 'Failed to fetch user data', {
userId,
error: error.message,
})
throw error
}
})
Basic error reporting without external dependencies:
// utils/error-reporter.ts
const errorStore = new Map<
string,
{ count: number; lastSeen: Date; error: any }
>()
export function reportError(error: Error, context?: any) {
const key = `${error.name}:${error.message}`
const existing = errorStore.get(key)
if (existing) {
existing.count++
existing.lastSeen = new Date()
} else {
errorStore.set(key, {
count: 1,
lastSeen: new Date(),
error: {
name: error.name,
message: error.message,
stack: error.stack,
context,
},
})
}
// Log immediately
console.error('[ERROR REPORTED]:', {
error: error.message,
count: existing ? existing.count : 1,
context,
})
}
// Error reporting endpoint
// routes/errors.ts
export const Route = createFileRoute('/admin/errors')({
server: {
handlers: {
GET: async () => {
const errors = Array.from(errorStore.entries()).map(([key, data]) => ({
id: key,
...data,
}))
return json({ errors })
},
},
},
})
// utils/error-reporter.ts
const errorStore = new Map<
string,
{ count: number; lastSeen: Date; error: any }
>()
export function reportError(error: Error, context?: any) {
const key = `${error.name}:${error.message}`
const existing = errorStore.get(key)
if (existing) {
existing.count++
existing.lastSeen = new Date()
} else {
errorStore.set(key, {
count: 1,
lastSeen: new Date(),
error: {
name: error.name,
message: error.message,
stack: error.stack,
context,
},
})
}
// Log immediately
console.error('[ERROR REPORTED]:', {
error: error.message,
count: existing ? existing.count : 1,
context,
})
}
// Error reporting endpoint
// routes/errors.ts
export const Route = createFileRoute('/admin/errors')({
server: {
handlers: {
GET: async () => {
const errors = Array.from(errorStore.entries()).map(([key, data]) => ({
id: key,
...data,
}))
return json({ errors })
},
},
},
})
While TanStack Start provides built-in observability patterns, external tools offer more comprehensive monitoring:
Application Performance Monitoring:
Error Tracking:
Analytics & User Behavior:
OpenTelemetry is the industry standard for observability. Here's an experimental approach to integrate it with TanStack Start:
// instrumentation.ts - Initialize before your app
import { NodeSDK } from '@opentelemetry/sdk-node'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
import { Resource } from '@opentelemetry/resources'
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'tanstack-start-app',
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
}),
instrumentations: [getNodeAutoInstrumentations()],
})
// Initialize BEFORE importing your app
sdk.start()
// instrumentation.ts - Initialize before your app
import { NodeSDK } from '@opentelemetry/sdk-node'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
import { Resource } from '@opentelemetry/resources'
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'tanstack-start-app',
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
}),
instrumentations: [getNodeAutoInstrumentations()],
})
// Initialize BEFORE importing your app
sdk.start()
// Server function tracing
import { trace, SpanStatusCode } from '@opentelemetry/api'
const tracer = trace.getTracer('tanstack-start')
const getUserWithTracing = createServerFn({ method: 'GET' })
.inputValidator((id: string) => id)
.handler(async ({ data: id }) => {
return tracer.startActiveSpan('get-user', async (span) => {
span.setAttributes({
'user.id': id,
operation: 'database.query',
})
try {
const user = await db.users.findUnique({ where: { id } })
span.setStatus({ code: SpanStatusCode.OK })
return user
} catch (error) {
span.recordException(error)
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
})
throw error
} finally {
span.end()
}
})
})
// Server function tracing
import { trace, SpanStatusCode } from '@opentelemetry/api'
const tracer = trace.getTracer('tanstack-start')
const getUserWithTracing = createServerFn({ method: 'GET' })
.inputValidator((id: string) => id)
.handler(async ({ data: id }) => {
return tracer.startActiveSpan('get-user', async (span) => {
span.setAttributes({
'user.id': id,
operation: 'database.query',
})
try {
const user = await db.users.findUnique({ where: { id } })
span.setStatus({ code: SpanStatusCode.OK })
return user
} catch (error) {
span.recordException(error)
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
})
throw error
} finally {
span.end()
}
})
})
// Middleware for automatic tracing
import { createMiddleware } from '@tanstack/react-start'
import { trace, SpanStatusCode } from '@opentelemetry/api'
const tracer = trace.getTracer('tanstack-start')
const tracingMiddleware = createMiddleware().handler(
async ({ next, request }) => {
const url = new URL(request.url)
return tracer.startActiveSpan(
`${request.method} ${url.pathname}`,
async (span) => {
span.setAttributes({
'http.method': request.method,
'http.url': request.url,
'http.route': url.pathname,
})
try {
const response = await next()
span.setAttribute('http.status_code', response.status)
span.setStatus({ code: SpanStatusCode.OK })
return response
} catch (error) {
span.recordException(error)
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
})
throw error
} finally {
span.end()
}
},
)
},
)
// Middleware for automatic tracing
import { createMiddleware } from '@tanstack/react-start'
import { trace, SpanStatusCode } from '@opentelemetry/api'
const tracer = trace.getTracer('tanstack-start')
const tracingMiddleware = createMiddleware().handler(
async ({ next, request }) => {
const url = new URL(request.url)
return tracer.startActiveSpan(
`${request.method} ${url.pathname}`,
async (span) => {
span.setAttributes({
'http.method': request.method,
'http.url': request.url,
'http.route': url.pathname,
})
try {
const response = await next()
span.setAttribute('http.status_code', response.status)
span.setStatus({ code: SpanStatusCode.OK })
return response
} catch (error) {
span.recordException(error)
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
})
throw error
} finally {
span.end()
}
},
)
},
)
Note: The above OpenTelemetry integration is experimental and requires manual setup. We're exploring first-class OpenTelemetry support that would provide automatic instrumentation for server functions, middleware, and route loaders.
Most observability tools follow a similar integration pattern with TanStack Start:
// Initialize in app entry point
import { initObservabilityTool } from 'your-tool'
initObservabilityTool({
dsn: import.meta.env.VITE_TOOL_DSN,
environment: import.meta.env.NODE_ENV,
})
// Server function middleware
const observabilityMiddleware = createMiddleware().handler(async ({ next }) => {
return yourTool.withTracing('server-function', async () => {
try {
return await next()
} catch (error) {
yourTool.captureException(error)
throw error
}
})
})
// Initialize in app entry point
import { initObservabilityTool } from 'your-tool'
initObservabilityTool({
dsn: import.meta.env.VITE_TOOL_DSN,
environment: import.meta.env.NODE_ENV,
})
// Server function middleware
const observabilityMiddleware = createMiddleware().handler(async ({ next }) => {
return yourTool.withTracing('server-function', async () => {
try {
return await next()
} catch (error) {
yourTool.captureException(error)
throw error
}
})
})
// Different strategies per environment
const observabilityConfig = {
development: {
logLevel: 'debug',
enableTracing: true,
enableMetrics: false, // Too noisy in dev
},
production: {
logLevel: 'warn',
enableTracing: true,
enableMetrics: true,
enableAlerting: true,
},
}
// Different strategies per environment
const observabilityConfig = {
development: {
logLevel: 'debug',
enableTracing: true,
enableMetrics: false, // Too noisy in dev
},
production: {
logLevel: 'warn',
enableTracing: true,
enableMetrics: true,
enableAlerting: true,
},
}
Direct OpenTelemetry support is coming to TanStack Start, which will provide automatic instrumentation for server functions, middleware, and route loaders without the manual setup shown above.
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.