Skip to content

Next.js Integration ​

Due to Next.js's full-stack architecture, we need different approaches for the client and server sides to maintain security.

The Challenge ​

Next.js applications run code in two environments:

  • Client-side (Browser): React components, hooks, and client-side logic
  • Server-side (Node.js): API routes, server components, and server actions

The problem: Using a single SDK would risk exposing your API secret key in client-side code, which is a critical security vulnerability.

The solution: Use different SDKs for each environment:

  • React SDK for client-side logging (no API key required)
  • Node.js SDK for server-side logging (with API key)

Installation ​

Install both SDKs:

bash
npm install github:devatlogstyx/logstyx-js-react#release
npm install github:devatlogstyx/logstyx-js-node#release

Environment Setup ​

Create a .env.local file in your project root:

bash
# Public variables (exposed to browser)
NEXT_PUBLIC_LOGSTYX_PROJECT_ID=proj_abc123xyz
NEXT_PUBLIC_LOGSTYX_ENDPOINT=https://logs.yourcompany.com/api/v1/logs

# Private variables (server-side only)
LOGSTYX_PROJECT_SECRET=your_api_key_here

Security Critical

Never use NEXT_PUBLIC_ prefix for your API secret! Variables with this prefix are exposed to the browser.

Origin Configuration

Add your domain to allowed origins in Logstyx dashboard:

  • Development: http://localhost:3000
  • Production: https://yourdomain.com

Go to: Project Settings → Security → Allowed Origins

Client-Side Logging ​

Use the React SDK for all client-side components and interactions.

App Router (app/) ​

jsx
// app/providers.jsx
'use client';

import { createContext, useContext } from 'react';
import useLogstyx from 'logstyx-js-react';

const LoggerContext = createContext(null);

export function LoggerProvider({ children }) {
  const logger = useLogstyx({
    projectId: process.env.NEXT_PUBLIC_LOGSTYX_PROJECT_ID,
    endpoint: process.env.NEXT_PUBLIC_LOGSTYX_ENDPOINT,
    captureUncaught: true,
    captureUnhandledRejections: true
  });

  const { ErrorBoundary } = logger;

  return (
    <LoggerContext.Provider value={logger}>
      <ErrorBoundary logger={logger}>
        {children}
      </ErrorBoundary>
    </LoggerContext.Provider>
  );
}

export function useLogger() {
  const logger = useContext(LoggerContext);
  if (!logger) {
    throw new Error('useLogger must be used within LoggerProvider');
  }
  return logger;
}
jsx
// app/layout.jsx
import { LoggerProvider } from './providers';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <LoggerProvider>
          {children}
        </LoggerProvider>
      </body>
    </html>
  );
}
jsx
// app/page.jsx
'use client';

import { useLogger } from './providers';

export default function HomePage() {
  const logger = useLogger();
  const { Trackable } = logger;

  const handleAction = async () => {
    logger.info({ 
      message: 'Button clicked',
      page: 'home' 
    });
  };

  return (
    <div>
      <h1>Welcome</h1>
      
      <Trackable 
        event="click" 
        context={{ page: 'home' }}
        data={{ action: 'cta-click' }}>
        <button>Get Started</button>
      </Trackable>

      <button onClick={handleAction}>
        Manual Log
      </button>
    </div>
  );
}

Pages Router (pages/) ​

jsx
// pages/_app.jsx
import { createContext, useContext } from 'react';
import useLogstyx from 'logstyx-js-react';

const LoggerContext = createContext(null);

export function useLogger() {
  const logger = useContext(LoggerContext);
  if (!logger) {
    throw new Error('useLogger must be used within LoggerProvider');
  }
  return logger;
}

function MyApp({ Component, pageProps }) {
  const logger = useLogstyx({
    projectId: process.env.NEXT_PUBLIC_LOGSTYX_PROJECT_ID,
    endpoint: process.env.NEXT_PUBLIC_LOGSTYX_ENDPOINT,
    captureUncaught: true,
    captureUnhandledRejections: true
  });

  const { ErrorBoundary } = logger;

  return (
    <LoggerContext.Provider value={logger}>
      <ErrorBoundary logger={logger}>
        <Component {...pageProps} />
      </ErrorBoundary>
    </LoggerContext.Provider>
  );
}

export default MyApp;
jsx
// pages/index.jsx
import { useLogger } from './_app';

export default function HomePage() {
  const logger = useLogger();
  const { Trackable } = logger;

  return (
    <div>
      <Trackable event="click" data={{ action: 'signup' }}>
        <button>Sign Up</button>
      </Trackable>
    </div>
  );
}

Server-Side Logging ​

Use the Node.js SDK for API routes, server components, and server actions.

Setting Up the Logger ​

Create a shared logger instance for server-side code:

javascript
// lib/server-logger.js
import useLogstyx from 'logstyx-js-node';

const logger = useLogstyx({
  projectId: process.env.NEXT_PUBLIC_LOGSTYX_PROJECT_ID,
  apiKey: process.env.LOGSTYX_PROJECT_SECRET,
  endpoint: process.env.NEXT_PUBLIC_LOGSTYX_ENDPOINT,
  captureUncaught: true,
  captureUnhandledRejections: true
});

// Set application-wide context
logger.setContext({
  service: 'NextJS App',
  environment: process.env.NODE_ENV
});

export default logger;

Create a wrapper to automatically log all API requests:

javascript
// lib/rest-handler.js
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import logger from './server-logger';

const SUCCESS_LOG_LEVEL = 'info';
const ERROR_LOG_LEVEL = 'error';

// Redact sensitive fields from objects
function redactObject(obj) {
  if (!obj || typeof obj !== 'object') return obj;
  
  const redacted = { ...obj };
  const sensitiveFields = ['password', 'token', 'secret', 'apiKey', 'creditCard'];
  
  for (const key of Object.keys(redacted)) {
    if (sensitiveFields.some(field => 
      key.toLowerCase().includes(field.toLowerCase())
    )) {
      redacted[key] = '[REDACTED]';
    }
  }
  
  return redacted;
}

export function RestHandler(handler) {
  return async (request, context) => {
    const { searchParams, pathname } = new URL(request.url);
    const cookieStore = cookies();

    let body = null;
    let query = Object.fromEntries(searchParams.entries());

    // Parse body for non-GET requests
    if (request.method !== 'GET' && request.method !== 'HEAD') {
      try {
        const clonedRequest = request.clone();
        body = await clonedRequest.json().catch(() => null);
      } catch {
        body = null;
      }
    }

    try {
      // Call the actual handler
      const response = await handler(request, context);

      // Clone and read response (safely handle non-JSON responses)
      const clonedResponse = response.clone();
      let responseData = null;
      
      try {
        responseData = await clonedResponse.json();
      } catch {
        // Response is not JSON, that's okay
        responseData = { contentType: response.headers.get('content-type') };
      }

      // Only log non-GET requests to reduce noise
      if (request.method !== 'GET' && request.method !== 'HEAD') {
        logger.send(SUCCESS_LOG_LEVEL, {
          title: `${request.method} ${pathname}`,
          message: 'Request completed successfully',
          context: {
            query: redactObject(query),
            body: redactObject(body),
            cookies: cookieStore.getAll(),
            response: responseData,
            statusCode: response.status
          }
        }).catch(console.error);
      }

      return response;

    } catch (error) {
      // Log error with full context
      logger.send(ERROR_LOG_LEVEL, {
        title: `${request.method} ${pathname}`,
        message: error?.message || 'Unknown error',
        context: {
          stack: error?.stack,
          query: redactObject(query),
          body: redactObject(body),
          cookies: cookieStore.getAll()
        }
      }).catch(console.error);

      return NextResponse.json(
        { 
          error: error?.message || 'Internal server error',
          code: 'SERVER_ERROR'
        },
        { status: error?.response?.status || 500 }
      );
    }
  };
}

export { logger };

Using RestHandler in API Routes ​

App Router ​

javascript
// app/api/users/route.js
import { NextResponse } from 'next/server';
import { RestHandler } from '@/lib/rest-handler';

export const GET = RestHandler(async (request, { params }) => {
  // Your business logic here
  const users = await fetchUsers();

  return NextResponse.json({
    success: true,
    data: users
  });
});

export const POST = RestHandler(async (request, { params }) => {
  const body = await request.json();
  
  // Your business logic
  const newUser = await createUser(body);

  return NextResponse.json({
    success: true,
    data: newUser
  });
});

Pages Router ​

For Pages Router, create a separate handler since it uses a different API pattern:

javascript
// lib/pages-rest-handler.js
import logger from './server-logger';

const SUCCESS_LOG_LEVEL = 'info';
const ERROR_LOG_LEVEL = 'error';

function redactObject(obj) {
  if (!obj || typeof obj !== 'object') return obj;
  
  const redacted = { ...obj };
  const sensitiveFields = ['password', 'token', 'secret', 'apiKey', 'creditCard'];
  
  for (const key of Object.keys(redacted)) {
    if (sensitiveFields.some(field => 
      key.toLowerCase().includes(field.toLowerCase())
    )) {
      redacted[key] = '[REDACTED]';
    }
  }
  
  return redacted;
}

export function PagesRestHandler(handler) {
  return async (req, res) => {
    const startTime = Date.now();

    try {
      // Call the actual handler
      await handler(req, res);

      // Only log non-GET requests
      if (req.method !== 'GET' && req.method !== 'HEAD') {
        logger.send(SUCCESS_LOG_LEVEL, {
          title: `${req.method} ${req.url}`,
          message: 'Request completed successfully',
          context: {
            query: redactObject(req.query),
            body: redactObject(req.body),
            statusCode: res.statusCode,
            duration: Date.now() - startTime
          }
        }).catch(console.error);
      }

    } catch (error) {
      logger.send(ERROR_LOG_LEVEL, {
        title: `${req.method} ${req.url}`,
        message: error?.message || 'Unknown error',
        context: {
          stack: error?.stack,
          query: redactObject(req.query),
          body: redactObject(req.body),
          duration: Date.now() - startTime
        }
      }).catch(console.error);

      // Re-throw to let Next.js handle it
      if (!res.headersSent) {
        res.status(500).json({
          error: error?.message || 'Internal server error',
          code: 'SERVER_ERROR'
        });
      }
    }
  };
}

export { logger };
javascript
// pages/api/users/index.js
import { PagesRestHandler } from '@/lib/pages-rest-handler';

async function handler(req, res) {
  if (req.method === 'GET') {
    const users = await fetchUsers();
    res.status(200).json({ success: true, data: users });
  } else if (req.method === 'POST') {
    const newUser = await createUser(req.body);
    res.status(201).json({ success: true, data: newUser });
  } else {
    res.status(405).json({ error: 'Method not allowed' });
  }
}

export default PagesRestHandler(handler);

Server Actions ​

javascript
// app/actions/user-actions.js
'use server';

import logger from '@/lib/server-logger';

export async function updateUserProfile(formData) {
  try {
    logger.info({
      message: 'Updating user profile',
      userId: formData.get('userId')
    });

    const result = await saveUserProfile(formData);

    logger.info({
      message: 'Profile updated successfully',
      userId: formData.get('userId')
    });

    return { success: true, data: result };
  } catch (error) {
    logger.error({
      message: 'Profile update failed',
      error: error.message,
      userId: formData.get('userId')
    });

    return { success: false, error: error.message };
  }
}

Server Components ​

jsx
// app/dashboard/page.jsx
import logger from '@/lib/server-logger';

export default async function DashboardPage() {
  try {
    logger.info({ message: 'Loading dashboard' });
    
    const data = await fetchDashboardData();
    
    return (
      <div>
        <h1>Dashboard</h1>
        {/* Render data */}
      </div>
    );
  } catch (error) {
    logger.error({
      message: 'Dashboard load failed',
      error: error.message
    });
    
    throw error;
  }
}

Complete Example ​

Here's a full example showing both client and server-side logging:

Directory Structure ​

my-nextjs-app/
├── app/
│   ├── api/
│   │   └── users/
│   │       └── route.js          # API route with logging
│   ├── actions/
│   │   └── user-actions.js       # Server actions
│   ├── layout.jsx                # Root layout with provider
│   ├── page.jsx                  # Home page
│   └── providers.jsx             # Client logger provider
├── lib/
│   ├── server-logger.js          # Server-side logger
│   ├── rest-handler.js           # API handler wrapper (App Router)
│   └── pages-rest-handler.js     # API handler wrapper (Pages Router)
└── .env.local                    # Environment variables

Implementation ​

javascript
// lib/server-logger.js
import useLogstyx from 'logstyx-js-node';

const logger = useLogstyx({
  projectId: process.env.NEXT_PUBLIC_LOGSTYX_PROJECT_ID,
  apiKey: process.env.LOGSTYX_PROJECT_SECRET,
  endpoint: process.env.NEXT_PUBLIC_LOGSTYX_ENDPOINT,
  captureUncaught: process.env.NODE_ENV === 'production',
  captureUnhandledRejections: process.env.NODE_ENV === 'production'
});

logger.setContext({
  service: 'E-commerce Platform',
  environment: process.env.NODE_ENV,
  version: '1.0.0'
});

export default logger;
jsx
// app/providers.jsx
'use client';

import { createContext, useContext } from 'react';
import useLogstyx from 'logstyx-js-react';

const LoggerContext = createContext(null);

export function LoggerProvider({ children }) {
  const logger = useLogstyx({
    projectId: process.env.NEXT_PUBLIC_LOGSTYX_PROJECT_ID,
    endpoint: process.env.NEXT_PUBLIC_LOGSTYX_ENDPOINT,
    captureUncaught: true,
    captureUnhandledRejections: true
  });

  const { ErrorBoundary } = logger;

  return (
    <LoggerContext.Provider value={logger}>
      <ErrorBoundary logger={logger}>
        {children}
      </ErrorBoundary>
    </LoggerContext.Provider>
  );
}

export function useLogger() {
  const logger = useContext(LoggerContext);
  if (!logger) {
    throw new Error('useLogger must be used within LoggerProvider');
  }
  return logger;
}
jsx
// app/page.jsx
'use client';

import { useLogger } from './providers';

export default function HomePage() {
  const logger = useLogger();
  const { Trackable } = logger;

  const handleCheckout = async () => {
    logger.info({
      message: 'Checkout initiated',
      page: 'home'
    });

    try {
      const response = await fetch('/api/checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ items: [...] })
      });

      const data = await response.json();

      logger.info({
        message: 'Checkout completed',
        orderId: data.orderId
      });
    } catch (error) {
      logger.error({
        message: 'Checkout failed',
        error: error.message
      });
    }
  };

  return (
    <div>
      <h1>Shop Now</h1>
      
      <Trackable 
        event="click"
        context={{ page: 'home', section: 'hero' }}
        data={{ action: 'view-products' }}>
        <button>Browse Products</button>
      </Trackable>

      <button onClick={handleCheckout}>
        Checkout
      </button>
    </div>
  );
}
javascript
// app/api/checkout/route.js
import { NextResponse } from 'next/server';
import { RestHandler } from '@/lib/rest-handler';

export const POST = RestHandler(async (request) => {
  const body = await request.json();
  
  // Process checkout
  const order = await processCheckout(body.items);
  
  return NextResponse.json({
    success: true,
    orderId: order.id,
    total: order.total
  });
});

Best Practices ​

1. Never Expose API Secrets ​

javascript
// ❌ WRONG - Exposes secret to browser
const logger = useLogstyx({
  apiKey: process.env.LOGSTYX_PROJECT_SECRET // NEVER do this in client code!
});

// ✅ CORRECT - Client-side (no API key)
const logger = useLogstyx({
  projectId: process.env.NEXT_PUBLIC_LOGSTYX_PROJECT_ID,
  endpoint: process.env.NEXT_PUBLIC_LOGSTYX_ENDPOINT
});

// ✅ CORRECT - Server-side only
import logger from '@/lib/server-logger'; // Safe, runs server-side only

2. Use Environment-Specific Configuration ​

javascript
// lib/server-logger.js
const logger = useLogstyx({
  projectId: process.env.NEXT_PUBLIC_LOGSTYX_PROJECT_ID,
  apiKey: process.env.LOGSTYX_PROJECT_SECRET,
  endpoint: process.env.NEXT_PUBLIC_LOGSTYX_ENDPOINT,
  // Only capture errors in production
  captureUncaught: process.env.NODE_ENV === 'production',
  captureUnhandledRejections: process.env.NODE_ENV === 'production'
});

3. Redact Sensitive Data ​

Always redact passwords, tokens, and PII:

javascript
function redactObject(obj) {
  if (!obj || typeof obj !== 'object') return obj;
  
  const redacted = { ...obj };
  const sensitiveFields = [
    'password', 
    'token', 
    'secret', 
    'apiKey', 
    'creditCard',
    'ssn'
  ];
  
  for (const key of Object.keys(redacted)) {
    if (sensitiveFields.some(field => 
      key.toLowerCase().includes(field.toLowerCase())
    )) {
      redacted[key] = '[REDACTED]';
    }
  }
  
  return redacted;
}

4. Set Application Context ​

Provide helpful context at the application level:

javascript
// lib/server-logger.js
logger.setContext({
  service: 'API Server',
  environment: process.env.NODE_ENV,
  version: process.env.npm_package_version,
  region: process.env.VERCEL_REGION
});

5. Structure Your Logs ​

Use consistent structure for better searchability:

javascript
// ❌ Avoid
logger.info('User logged in');

// ✅ Better
logger.info({
  message: 'User logged in',
  userId: user.id,
  method: 'oauth',
  timestamp: Date.now()
});

6. Log Request Context in API Routes ​

Include useful request information:

javascript
logger.info({
  message: 'API request',
  method: request.method,
  path: pathname,
  userAgent: request.headers.get('user-agent'),
  ip: request.headers.get('x-forwarded-for')
});

Troubleshooting ​

API Key Exposed Error ​

Issue: Getting security warnings about exposed API keys

Solution: Ensure you're only using the API key in server-side code:

  • Server components (no 'use client' directive)
  • API routes
  • Server actions
  • lib/server-logger.js

Never import logstyx-js-node in client components.

CORS Errors ​

Issue: Getting CORS errors when logging from browser

Solution: Add your domains to allowed origins in Logstyx dashboard:

  1. Go to Project Detail → Settings
  2. Add origins:
    • http://localhost:3000 (development)
    • https://yourdomain.com (production)

Logs Not Appearing ​

Issue: Logs aren't showing up in dashboard

Solution: Check that:

  • Environment variables are set correctly
  • NEXT_PUBLIC_ prefix is used for client-side variables
  • API endpoint is correct and accessible
  • Project ID matches your Logstyx project

Double Logging in Development ​

Issue: Seeing duplicate logs in development

Solution: This is normal in development due to React Strict Mode. It won't happen in production.

Migration from Other Logging Solutions ​

From console.log ​

javascript
// Before
console.log('User logged in:', userId);
console.error('Failed to load data:', error);

// After - Client
const logger = useLogger();
logger.info({ message: 'User logged in', userId });
logger.error({ message: 'Failed to load data', error: error.message });

// After - Server
import logger from '@/lib/server-logger';
logger.info({ message: 'User logged in', userId });
logger.error({ message: 'Failed to load data', error: error.message });

From Winston/Pino ​

javascript
// Before
const winston = require('winston');
const logger = winston.createLogger({...});
logger.info('Request processed', { userId, duration });

// After
import logger from '@/lib/server-logger';
logger.info({ 
  message: 'Request processed', 
  userId, 
  duration 
});

Additional Resources ​

Released under the MIT License.